@claude-flow/cli 3.65.1 → 3.66.0

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.
@@ -0,0 +1,594 @@
1
+ // GENERATED by plugins/ruflo-mods/scripts/build-learning-loop-helper.mjs from the ruflo-mods learning loop (ADR-512). Do not edit.
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // plugins/ruflo-mods/scripts/learning-loop-helper.ts
22
+ var learning_loop_helper_exports = {};
23
+ __export(learning_loop_helper_exports, {
24
+ MAX_READ: () => MAX_READ,
25
+ enabled: () => enabled,
26
+ ingestDue: () => ingestDue,
27
+ lessonsForPrompt: () => lessonsForPrompt,
28
+ recordTurns: () => recordTurns
29
+ });
30
+ module.exports = __toCommonJS(learning_loop_helper_exports);
31
+ var import_node_fs = require("node:fs");
32
+ var import_node_path = require("node:path");
33
+
34
+ // plugins/ruflo-mods/hooks/guidance/screen.ts
35
+ var SECRETS = [
36
+ ["private key", /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/],
37
+ ["aws access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/],
38
+ ["github token", /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,})\b/],
39
+ ["slack token", /\b(?:xox[abeprs]-|xapp-\d-)[A-Za-z0-9-]{10,}/],
40
+ ["google api key", /\bAIza[0-9A-Za-z_-]{35}(?:\b|(?![0-9A-Za-z_-]))/],
41
+ ["anthropic or openai key", /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{24,}/],
42
+ // One start per token run (linear on '-eyJ' repeats), at the first eyJ after a '-' or the run start.
43
+ ["jwt", /(?<![A-Za-z0-9_-])(?=((?:[A-Za-z0-9_]*-)*?eyJ))\1[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/],
44
+ ["bearer token", /\bBearer\s+[A-Za-z0-9._~+/=-]{24,}/],
45
+ // The keyword ends an _ or - separated name (GITHUB_TOKEN=, "client_secret":), optionally then _key / _access_key
46
+ // (aws_secret_access_key =). Names that only start with it (TOKEN_URL=, MAX_TOKENS=) and all-digit values do not
47
+ // match, and nothing after the keyword can backtrack against it (linear on 'token_token_...').
48
+ // A value that only names where the secret lives is not one: process.env.X / os.environ / import.meta.env.X, a
49
+ // ./ ../ or ~/ path to a file with an extension, a secret-manager reference name-with-separators:latest or :3, or
50
+ // NAME=that-reference (--set-secrets=NAME=ref:latest). A literal value is never excluded by its shape alone.
51
+ ["key assignment", /(?<![A-Za-z0-9_-])[_-]*(?:[A-Za-z0-9]+[_-]+)*(?:api[_-]?key|secret|token|passw(?:or)?d|credential)s?(?:[_-](?:access[_-]?)?key)?["']?\s*[:=]\s*["']?(?!process\.env\.|os\.environ\b|import\.meta\.env\.|(?:\.{1,2}|~)\/[A-Za-z0-9_./-]*\.[A-Za-z0-9]{1,8}(?![A-Za-z0-9/+=_.-])|[A-Za-z][A-Za-z0-9]*(?:[_-][A-Za-z0-9]+)+:(?:latest|\d{1,4})\b|[A-Za-z][A-Za-z0-9]*(?:[_-][A-Za-z0-9]+)*=[A-Za-z][A-Za-z0-9]*(?:[_-][A-Za-z0-9]+)+:(?:latest|\d{1,4})\b)(?=[A-Za-z0-9/+=_.-]*[A-Za-z])[A-Za-z0-9/+=_.-]{16,}/i]
52
+ ];
53
+ var INJECTION = [
54
+ ["override instructions", /\b(?:ignore|disregard|forget|override)\b[^.\n]{0,40}\b(?:previous|prior|above|earlier|all|any|system)\b[^.\n]{0,30}\b(?:instructions?|rules?|prompts?|guidelines?)\b/i],
55
+ ["role reassignment", /\byou are (?:now|no longer)\b|\bact as (?:an? )?(?:unrestricted|jailbroken)\b/i],
56
+ ["new instructions", /\b(?:new|updated|real) (?:system )?instructions?\s*:/i],
57
+ ["fake role tags", /<\/?\s*(?:system|assistant|developer|instructions?)\s*>|^\s*(?:system|assistant)\s*:/im],
58
+ ["concealment", /\bdo not (?:tell|inform|mention|reveal)[^.\n]{0,30}\b(?:user|human|operator)\b/i],
59
+ // `.env` has no word start before its dot, so it is matched on its own ("upload the .env file", "send ./config.env",
60
+ // ".envrc"); a dot inside a word does not end the phrase. A model's token counts are not credentials: --max-tokens,
61
+ // /oauth/token and "token usage/count/limit/budget/window" are left out (a file named token, /run/token, is not).
62
+ ["exfiltration", /\b(?:exfiltrate|send|post|upload|e-?mail)\b(?:[^.\n]|\.(?=\S)){0,50}(?:\b(?:secrets?|credentials?|api keys?)\b|(?:(?<!max[_-]?)\btokens\b|(?<!oauth\/)\btoken\b)(?![\s_-]*(?:usage|counts?|limits?|budgets?|window)\b)|\.env(?:rc)?\b)/i],
63
+ ["shell pipe", /\b(?:curl|wget)\b[^|\n]{0,200}\|\s*(?:sudo\s+)?(?:ba|z)?sh\b/i]
64
+ ];
65
+ var INVISIBLE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u2028\u2029\p{Default_Ignorable_Code_Point}]/gu;
66
+ var LEGACY_INVISIBLE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060-\u2064\ufeff]/g;
67
+ var collapse = (text) => text.replace(/\s+/g, (run) => run.includes("\n") ? "\n" : " ");
68
+ var bare = (text) => collapse(text.replace(INVISIBLE, "").normalize("NFKC").replace(INVISIBLE, ""));
69
+ var readings = (text) => [bare(text), collapse(text.replace(LEGACY_INVISIBLE, "")), collapse(text.replace(INVISIBLE, " "))];
70
+ var WINDOW = 2e4;
71
+ var OVERLAP = 1024;
72
+ function cut(clean) {
73
+ if (clean.length <= WINDOW) return [clean];
74
+ const out = [];
75
+ for (let start2 = 0; start2 < clean.length; start2 += WINDOW - OVERLAP) {
76
+ const ws = start2 === 0 ? -1 : clean.slice(start2, start2 + 256).search(/\s/);
77
+ const from = ws >= 0 ? start2 + ws + 1 : start2;
78
+ const lead = ws >= 0 && clean[from - 1] !== "\n" ? "" : "";
79
+ out.push(lead + clean.slice(from, start2 + WINDOW));
80
+ if (start2 + WINDOW >= clean.length) break;
81
+ }
82
+ return out;
83
+ }
84
+ function windows(text) {
85
+ return readings(text).flatMap(cut);
86
+ }
87
+ var anyWindow = (parts2, re) => parts2.some((part) => re.test(part));
88
+ var names = (rules, parts2) => rules.filter(([, re]) => anyWindow(parts2, re)).map(([name]) => name);
89
+ function scanWindows(parts2) {
90
+ return { secrets: names(SECRETS, parts2), injection: names(INJECTION, parts2) };
91
+ }
92
+ var scan = (text) => scanWindows(windows(text));
93
+
94
+ // plugins/ruflo-mods/hooks/compact-snapshot/snapshot.ts
95
+ var MAX_BYTES = 2 * 1024 * 1024;
96
+ function utf8Bytes(text) {
97
+ let n = 0;
98
+ for (let i = 0; i < text.length; i++) {
99
+ const c = text.charCodeAt(i);
100
+ if (c < 128) n += 1;
101
+ else if (c < 2048) n += 2;
102
+ else if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
103
+ n += 4;
104
+ i++;
105
+ } else n += 3;
106
+ }
107
+ return n;
108
+ }
109
+
110
+ // plugins/ruflo-mods/hooks/loop/taint.ts
111
+ var TEST_FILE = /(?:\.(?:test|spec)\.[A-Za-z0-9]+|^test_[^/]*\.py|_test\.(?:go|rs))$/;
112
+ var TEST_DIRS = /* @__PURE__ */ new Set(["__tests__", "tests"]);
113
+ var VERIFIER_CONFIG = /^(?:package\.json|vitest\.config\.[^/]+|jest\.config\.[^/]+|pytest\.ini|pyproject\.toml|setup\.cfg|tox\.ini|Cargo\.toml|go\.mod|tsconfig[^/]*\.json|\.eslintrc[^/]*|eslint\.config\.[^/]+|Makefile)$/;
114
+ var parts = (path) => path.replace(/\\/g, "/").split("/").filter((p) => p !== "" && p !== ".");
115
+ function relativePath(root, path) {
116
+ const p = path.replace(/\\/g, "/");
117
+ const r = root.replace(/\\/g, "/").replace(/\/+$/, "");
118
+ return r && p.startsWith(`${r}/`) ? p.slice(r.length + 1) : p;
119
+ }
120
+ function isTestPath(path) {
121
+ const segs = parts(path);
122
+ const base = segs.at(-1);
123
+ if (base === void 0) return false;
124
+ return TEST_FILE.test(base) || segs.slice(0, -1).some((d) => TEST_DIRS.has(d));
125
+ }
126
+ function isVerifierConfig(path) {
127
+ const base = parts(path).at(-1);
128
+ return base !== void 0 && VERIFIER_CONFIG.test(base);
129
+ }
130
+ var WEAKEN = /^(?:--passWithNoTests|--exclude|--testPathIgnorePatterns)(?:=|$)/;
131
+ var SELECT = /^(?:-t|--testNamePattern|-k|--skip|-run)(?:=|$)/;
132
+ var unquote = (token) => token.replace(/^['"]|['"]$/g, "");
133
+ function narrows(segment, kind) {
134
+ const tokens = segment.split(/\s+/).map(unquote).filter(Boolean);
135
+ if (tokens.some((t) => WEAKEN.test(t))) return true;
136
+ if (kind !== "test") return false;
137
+ if (tokens.some((t) => SELECT.test(t))) return true;
138
+ return tokens.some((t) => !t.startsWith("-") && (t.includes("::") || isTestPath(t.split("::")[0])));
139
+ }
140
+ var WRITERS = /* @__PURE__ */ new Set(["rm", "unlink", "shred", "mv", "cp", "tee", "truncate", "patch", "dd", "ln", "install", "rsync", "touch"]);
141
+ var GIT_WRITERS = /* @__PURE__ */ new Set(["rm", "mv", "checkout", "restore", "apply", "am", "reset", "stash", "clean"]);
142
+ var WRAPPERS = /* @__PURE__ */ new Set(["sudo", "env", "command", "xargs", "time", "nice", "nohup"]);
143
+ var IN_PLACE = /^(?:-[A-Za-z]*i|--in-place)/;
144
+ var REDIRECT = /(?:^|[^<>&])\d*>>?(?!&)/;
145
+ function writes(tokens) {
146
+ if (tokens.some((t) => REDIRECT.test(t))) return true;
147
+ let i = 0;
148
+ while (i < tokens.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]) || WRAPPERS.has(tokens[i]))) i++;
149
+ const prog = tokens[i]?.split("/").at(-1);
150
+ if (prog === void 0) return false;
151
+ if (WRITERS.has(prog)) return true;
152
+ if (prog === "sed" || prog === "perl") return tokens.slice(i + 1).some((t) => IN_PLACE.test(t));
153
+ if (prog === "git") return tokens.slice(i + 1).some((t) => GIT_WRITERS.has(t));
154
+ return false;
155
+ }
156
+ var namesTests = (token) => isTestPath(token) || parts(token).some((d) => TEST_DIRS.has(d));
157
+ function shellTouches(command) {
158
+ const out = { tests: false, config: false };
159
+ if (typeof command !== "string" || command.length > 16384) return out;
160
+ for (const segment of command.split(/&&|\|\||[;|\n]/)) {
161
+ const tokens = segment.split(/\s+/).map(unquote).filter(Boolean);
162
+ if (tokens.length === 0 || !writes(tokens)) continue;
163
+ for (const raw of tokens) {
164
+ const token = raw.replace(/^\d*>>?/, "");
165
+ if (!token || token.startsWith("-")) continue;
166
+ if (namesTests(token)) out.tests = true;
167
+ if (isVerifierConfig(token)) out.config = true;
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ // plugins/ruflo-mods/hooks/loop/record.ts
174
+ var VERIFIED_PATH = ".claude-flow/data/verified-trajectories.jsonl";
175
+ var LESSONS_PATH = ".claude-flow/data/learned-lessons.json";
176
+ var MAX_LINES = 500;
177
+ var MAX_STEPS = 64;
178
+ var MAX_TASK = 300;
179
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
180
+ var TOOL = /^[A-Za-z0-9_.:-]{1,80}$/;
181
+ function taskText(prompt) {
182
+ const flat = prompt.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}/g, "[email]").replace(/\s+/g, " ").trim().slice(0, MAX_TASK);
183
+ return scan(flat).secrets.length > 0 ? "[redacted]" : flat;
184
+ }
185
+ var SESSION = /^[A-Za-z0-9_-]{1,40}$/;
186
+ function start(id, prompt, agent, now, session = "") {
187
+ return {
188
+ id,
189
+ task: taskText(prompt),
190
+ agent: /^[a-z][a-z0-9-]{0,40}$/.test(agent) ? agent : "coder",
191
+ startedAt: now,
192
+ session: SESSION.test(session) ? session : "",
193
+ steps: [],
194
+ lastEdit: 0,
195
+ edits: 0,
196
+ runs: [],
197
+ seq: 0,
198
+ editedTests: false,
199
+ editedConfig: false,
200
+ files: []
201
+ };
202
+ }
203
+ var MAX_FILES = 5;
204
+ var FILE = /^[A-Za-z0-9_][A-Za-z0-9_./-]{0,119}$/;
205
+ var SENSITIVE = /(?:^|[._-])(?:env|secrets?|credentials?|passwords?|tokens?|private|id_rsa|id_ed25519)(?:[._-]|$)|\.(?:pem|key|p12|pfx|crt|keystore)$|^(?:npmrc|netrc|pypirc)$/i;
206
+ function lessonFile(path) {
207
+ if (!FILE.test(path)) return false;
208
+ const segs = path.split("/");
209
+ if (segs.some((s) => s === "" || s === ".." || s.startsWith("."))) return false;
210
+ if (SENSITIVE.test(segs.at(-1))) return false;
211
+ return !isTestPath(path) && !isVerifierConfig(path);
212
+ }
213
+ function step(t, tool, ok, verifier, passed, path, command) {
214
+ t.seq++;
215
+ if (t.steps.length < MAX_STEPS && TOOL.test(tool)) t.steps.push({ tool, ok });
216
+ if (tool === "Bash") {
217
+ const touched = shellTouches(command);
218
+ if (touched.tests) t.editedTests = true;
219
+ if (touched.config) t.editedConfig = true;
220
+ }
221
+ if (EDIT_TOOLS.has(tool) && ok) {
222
+ t.lastEdit = t.seq;
223
+ t.edits++;
224
+ if (typeof path === "string" && path.length > 0 && path.length <= 4096) {
225
+ if (isTestPath(path)) t.editedTests = true;
226
+ if (isVerifierConfig(path)) t.editedConfig = true;
227
+ if (t.files.length < MAX_FILES && !t.files.includes(path) && lessonFile(path)) t.files.push(path);
228
+ }
229
+ }
230
+ if (verifier && passed !== null) t.runs.push({ kind: verifier.kind, command: verifier.command, passed, seq: t.seq, narrowed: verifier.narrowed === true });
231
+ }
232
+ function decide(t, aborted) {
233
+ if (aborted || t.edits === 0) return { outcome: "unknown", run: null };
234
+ const after = t.runs.filter((r) => r.seq > t.lastEdit);
235
+ if (after.length === 0) return { outcome: "unknown", run: null };
236
+ const failed = after.find((r) => !r.passed);
237
+ return failed ? { outcome: "verified-failure", run: failed } : { outcome: "verified-success", run: after[after.length - 1] };
238
+ }
239
+ function taints(t, outcome, run) {
240
+ if (outcome !== "verified-success") return [];
241
+ const out = [];
242
+ if (t.editedTests) out.push("edited-tests");
243
+ if (t.editedConfig) out.push("edited-verifier-config");
244
+ if (run?.narrowed) out.push("narrowed-verifier");
245
+ return out;
246
+ }
247
+ function line(t, aborted, now) {
248
+ const { outcome, run } = decide(t, aborted);
249
+ return JSON.stringify({
250
+ v: 1,
251
+ id: t.id,
252
+ task: t.task,
253
+ agent: t.agent,
254
+ outcome,
255
+ verifier: run ? { kind: run.kind, command: run.command, passed: run.passed } : null,
256
+ steps: t.steps,
257
+ edits: t.edits,
258
+ taint: taints(t, outcome, run),
259
+ // ADR-511: a verified turn says which files it edited (the CLI names only a success's in a lesson).
260
+ ...outcome !== "unknown" && t.files.length > 0 ? { files: t.files } : {},
261
+ ...t.session ? { session: t.session } : {},
262
+ startedAt: t.startedAt,
263
+ endedAt: now
264
+ });
265
+ }
266
+ function appendLines(existing, lines) {
267
+ const all = [...existing.split("\n").filter((l) => l.trim()), ...lines];
268
+ return `${all.slice(-MAX_LINES).join("\n")}
269
+ `;
270
+ }
271
+
272
+ // plugins/ruflo-mods/hooks/loop/lessons.ts
273
+ var GATE = 0.6;
274
+ var MAX_PER_PROMPT = 3;
275
+ var MAX_LESSONS = 50;
276
+ var MAX_FILES_SHOWN = 3;
277
+ var WORD = /^[a-z0-9][a-z0-9 _./-]{0,40}$/;
278
+ var AGENT = /^[a-z][a-z0-9-]{0,40}$/;
279
+ var ID = /^L-[a-f0-9]{10}$/;
280
+ var EVIDENCE = /^[A-Za-z0-9 ._:/=@+-]{1,80}$/;
281
+ var count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.min(Math.floor(v), 1e6) : 0;
282
+ function parseLessons(text) {
283
+ const raw = JSON.parse(text);
284
+ if (!raw || typeof raw !== "object" || raw.version !== 1) throw new Error("learned-lessons.json: not version 1");
285
+ const gate = typeof raw.gate === "number" && raw.gate >= GATE && raw.gate < 1 ? raw.gate : GATE;
286
+ const lessons = [];
287
+ for (const item of Array.isArray(raw.lessons) ? raw.lessons.slice(0, MAX_LESSONS) : []) {
288
+ if (!item || typeof item !== "object") continue;
289
+ const l = item;
290
+ const confidence = typeof l.confidence === "number" && l.confidence >= 0 && l.confidence <= 1 ? l.confidence : -1;
291
+ const keywords = Array.isArray(l.keywords) ? l.keywords.filter((k) => typeof k === "string" && WORD.test(k)).slice(0, 10) : [];
292
+ if (typeof l.id !== "string" || !ID.test(l.id) || typeof l.agent !== "string" || !AGENT.test(l.agent) || keywords.length === 0 || confidence < 0) continue;
293
+ if (l.kind !== "lesson" && l.kind !== "caution") continue;
294
+ if (l.kind === "lesson" && confidence < gate) continue;
295
+ const evidence = typeof l.evidence === "string" && EVIDENCE.test(l.evidence) ? l.evidence : "";
296
+ const files = l.kind === "lesson" && Array.isArray(l.files) ? l.files.filter((f) => typeof f === "string" && lessonFile(f)).slice(0, MAX_FILES_SHOWN) : [];
297
+ lessons.push({ id: l.id, kind: l.kind, agent: l.agent, keywords, confidence, successes: count(l.successes), failures: count(l.failures), evidence, files });
298
+ }
299
+ return { gate, lessons, belowGate: count(raw.belowGate) };
300
+ }
301
+ var words = (text) => new Set(text.toLowerCase().split(/[\s\-_.,;:!?'"()\[\]{}]+/).filter(Boolean));
302
+ var says = (said, lower, keyword) => said.has(keyword) || /[ /]/.test(keyword) && lower.includes(keyword);
303
+ var MIN_SHARE_OF_BEST = 0.5;
304
+ function pick(file, prompt) {
305
+ const said = words(prompt);
306
+ const lower = prompt.toLowerCase();
307
+ const df = /* @__PURE__ */ new Map();
308
+ for (const l of file.lessons) for (const k of new Set(l.keywords)) df.set(k, (df.get(k) ?? 0) + 1);
309
+ const weight = (k) => Math.log((file.lessons.length + 1) / (df.get(k) ?? 1));
310
+ const sum = (ks) => ks.reduce((a, k) => a + weight(k), 0);
311
+ const scored = file.lessons.map((l) => {
312
+ const matched = l.keywords.filter((k) => says(said, lower, k));
313
+ return { l, overlap: matched.length, score: sum(matched) / sum(l.keywords) * l.confidence };
314
+ }).filter((x) => x.overlap > 0).sort((a, b) => b.score - a.score);
315
+ const best = scored[0]?.score ?? 0;
316
+ return scored.filter((x) => x.score >= best * MIN_SHARE_OF_BEST).slice(0, MAX_PER_PROMPT).map((x) => x.l);
317
+ }
318
+ function lessonBlock(chosen) {
319
+ if (chosen.length === 0) return null;
320
+ const lines = chosen.map((l) => {
321
+ const tag = `[${l.kind} ${l.id} conf ${l.confidence.toFixed(2)}, ${l.successes} passed / ${l.failures} failed]`;
322
+ const check = l.evidence ? ` \`${l.evidence}\`` : " verification";
323
+ if (l.kind === "caution") return `- ${tag} tasks about ${l.keywords.join(", ")} failed${check} ${l.failures} times: check that first`;
324
+ if (l.files.length) return `- ${tag} verified fixes for tasks like this (${l.keywords.join(", ")}) edited ${l.files.map((f) => `\`${f}\``).join(", ")}: look there first. They passed${check}.`;
325
+ return `- ${tag} tasks about ${l.keywords.join(", ")} went to ${l.agent} and passed${check}`;
326
+ });
327
+ const block = ["[RUFLO_LESSONS] From this project's own test/build exit status (never a model's claim of success). Advisory only:", ...lines].join("\n");
328
+ const found = scan(block);
329
+ return found.secrets.length || found.injection.length ? null : block;
330
+ }
331
+
332
+ // plugins/ruflo-mods/hooks/loop/verifier.ts
333
+ var MAX_EVIDENCE = 80;
334
+ var RUNNERS = "(?:npm|pnpm|yarn|bun)";
335
+ var SCRIPT_KIND = {
336
+ test: "test",
337
+ "test:unit": "test",
338
+ "test:integration": "test",
339
+ "test:e2e": "test",
340
+ "test:ci": "test",
341
+ build: "build",
342
+ typecheck: "typecheck",
343
+ "type-check": "typecheck",
344
+ tsc: "typecheck",
345
+ lint: "lint",
346
+ check: "typecheck"
347
+ };
348
+ var RULES = [
349
+ // npm test / pnpm test / yarn test / bun test (with or without run, workspace flags before the script)
350
+ [new RegExp(`^${RUNNERS}(?:\\s+(?:-r|--recursive|-w|--workspaces|--filter\\s+\\S+|--silent|-s))*\\s+(?:run\\s+)?([a-z][a-z:-]*)\\b`), (m) => SCRIPT_KIND[m[1]] ?? null],
351
+ [/^(?:npx\s+|pnpm\s+exec\s+|pnpm\s+dlx\s+|yarn\s+|bunx\s+)?(?:vitest|jest|mocha|ava|playwright\s+test)\b/, "test"],
352
+ [/^(?:npx\s+|pnpm\s+exec\s+|yarn\s+|bunx\s+)?tsc\b/, "typecheck"],
353
+ [/^(?:npx\s+|pnpm\s+exec\s+|yarn\s+|bunx\s+)?eslint\b/, "lint"],
354
+ [/^node\s+--test\b/, "test"],
355
+ [/^cargo\s+(?:\+\S+\s+)?(test|nextest|build|check|clippy)\b/, (m) => m[1] === "build" ? "build" : m[1] === "check" ? "typecheck" : m[1] === "clippy" ? "lint" : "test"],
356
+ [/^go\s+(test|build|vet)\b/, (m) => m[1] === "test" ? "test" : m[1] === "build" ? "build" : "lint"],
357
+ [/^(?:python3?\s+-m\s+)?pytest\b/, "test"],
358
+ [/^(?:uv\s+run\s+|poetry\s+run\s+)pytest\b/, "test"],
359
+ [/^(?:\.\/)?(?:mvnw?|gradlew?)\s+(?:\S+\s+)*?(test|verify|build|check)\b/, (m) => m[1] === "build" ? "build" : "test"],
360
+ [/^dotnet\s+(test|build)\b/, (m) => m[1] === "test" ? "test" : "build"],
361
+ [/^make\s+(test|check|build)\b/, (m) => m[1] === "build" ? "build" : "test"],
362
+ [/^(?:mypy|pyright)\b/, "typecheck"]
363
+ ];
364
+ var SETUP = /^(?:cd\s+\S+|export\s+[A-Za-z_][A-Za-z0-9_]*=\S*)$/;
365
+ var ENV_PREFIX = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)+/;
366
+ var REDIRECTS = /\s*(?:\d?>&\d|&>>?\s*\S+|\d?>>?\s*\S+|<\s*\S+)/g;
367
+ function classify(command) {
368
+ if (typeof command !== "string" || command.length === 0 || command.length > 2e3) return null;
369
+ const flat = command.replace(/\\\n/g, " ").trim();
370
+ if (/[\n\r`]|\$\(|\|\||;|\|/.test(flat)) return null;
371
+ const segments = flat.split("&&").map((s) => s.replace(REDIRECTS, "").trim());
372
+ if (segments.some((s) => s.length === 0 || s.includes("&"))) return null;
373
+ let found = null;
374
+ for (const segment of segments) {
375
+ if (SETUP.test(segment)) continue;
376
+ const bare2 = segment.replace(ENV_PREFIX, "");
377
+ const kind = kindOf(bare2);
378
+ if (kind === null) return null;
379
+ found = { kind: rank(found?.kind, kind), command: bare2.replace(/\s+/g, " ").slice(0, MAX_EVIDENCE), narrowed: found?.narrowed === true || narrows(bare2, kind) };
380
+ }
381
+ return found;
382
+ }
383
+ function kindOf(segment) {
384
+ for (const [re, kind] of RULES) {
385
+ const m = segment.match(re);
386
+ if (m) return typeof kind === "function" ? kind(m) : kind;
387
+ }
388
+ return null;
389
+ }
390
+ var ORDER = ["lint", "typecheck", "build", "test"];
391
+ var rank = (a, b) => a !== void 0 && ORDER.indexOf(a) > ORDER.indexOf(b) ? a : b;
392
+ function verdict(result) {
393
+ if (!result || typeof result !== "object") return null;
394
+ const r = result;
395
+ if (r.deny !== void 0) return null;
396
+ const out = r.result ?? {};
397
+ if (out.interrupted === true || typeof out.backgroundTaskId === "string" || typeof out.timedOutAfterMs === "number") return null;
398
+ return r.isError !== true;
399
+ }
400
+
401
+ // plugins/ruflo-mods/hooks/loop/transcript.ts
402
+ var INTERRUPTED = "[Request interrupted by user";
403
+ var blocks = (e) => Array.isArray(e.message?.content) ? e.message.content : [];
404
+ function promptOf(e) {
405
+ if (e.type !== "user" || e.isSidechain === true || e.isMeta === true || e.isCompactSummary === true) return null;
406
+ const content = e.message?.content;
407
+ const text = typeof content === "string" ? content : Array.isArray(content) && !content.some((b) => b.type === "tool_result") ? content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n") : null;
408
+ if (text === null) return null;
409
+ const trimmed = text.trimStart();
410
+ if (trimmed === "" || trimmed.startsWith("/") || trimmed.startsWith("<") || trimmed.startsWith(INTERRUPTED)) return null;
411
+ return text;
412
+ }
413
+ var time = (e, fallback) => {
414
+ const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : NaN;
415
+ return Number.isFinite(t) ? t : fallback;
416
+ };
417
+ function resultOf(block, extra) {
418
+ return { isError: block.is_error === true, result: extra && typeof extra === "object" ? extra : {} };
419
+ }
420
+ function replay(text, opts) {
421
+ const lines = [];
422
+ let open = null;
423
+ const calls = /* @__PURE__ */ new Map();
424
+ const close = () => {
425
+ if (open && open.t.steps.length > 0) lines.push(line(open.t, open.aborted, open.last));
426
+ open = null;
427
+ calls.clear();
428
+ };
429
+ const raws = text.split("\n");
430
+ raws.pop();
431
+ let pos = 0;
432
+ for (const raw of raws) {
433
+ const at = pos;
434
+ pos += utf8Bytes(raw) + 1;
435
+ if (!raw.trim()) continue;
436
+ let e;
437
+ try {
438
+ e = JSON.parse(raw);
439
+ } catch {
440
+ continue;
441
+ }
442
+ if (!e || typeof e !== "object" || e.isSidechain === true) continue;
443
+ const prompt = promptOf(e);
444
+ if (prompt !== null) {
445
+ close();
446
+ const when = time(e, opts.now);
447
+ const id = `vt-c${opts.session.replace(/[^A-Za-z0-9]/g, "").slice(0, 16)}-${(opts.base + at).toString(36)}`;
448
+ open = { t: start(id, prompt, opts.agentFor(prompt), when, opts.session), aborted: false, last: when, at };
449
+ continue;
450
+ }
451
+ const turn = open;
452
+ if (!turn) continue;
453
+ turn.last = time(e, turn.last);
454
+ for (const b of blocks(e)) {
455
+ if (e.type === "assistant" && b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") {
456
+ calls.set(b.id, { tool: b.name, input: b.input && typeof b.input === "object" ? b.input : {} });
457
+ } else if (e.type === "user" && b.type === "tool_result" && typeof b.tool_use_id === "string") {
458
+ const call = calls.get(b.tool_use_id);
459
+ if (!call) continue;
460
+ calls.delete(b.tool_use_id);
461
+ const command = call.tool === "Bash" ? call.input.command : void 0;
462
+ const verifier = call.tool === "Bash" ? classify(command) : null;
463
+ const target = typeof call.input.file_path === "string" ? call.input.file_path : typeof call.input.notebook_path === "string" ? call.input.notebook_path : void 0;
464
+ const status = verifier && call.input.run_in_background !== true ? verdict(resultOf(b, e.toolUseResult)) : null;
465
+ step(turn.t, call.tool, b.is_error !== true, verifier, status, target === void 0 ? void 0 : relativePath(opts.root, target), command);
466
+ } else if (e.type === "user" && b.type === "text" && typeof b.text === "string" && b.text.startsWith(INTERRUPTED)) {
467
+ turn.aborted = true;
468
+ }
469
+ }
470
+ }
471
+ const still = open;
472
+ if (opts.final || still === null) {
473
+ close();
474
+ return { lines, consumed: pos };
475
+ }
476
+ return { lines, consumed: still.at };
477
+ }
478
+ function lessonsFor(prompt, lessonsText) {
479
+ try {
480
+ return lessonBlock(pick(parseLessons(lessonsText), prompt)) ?? "";
481
+ } catch {
482
+ return "";
483
+ }
484
+ }
485
+
486
+ // plugins/ruflo-mods/scripts/learning-loop-helper.ts
487
+ var STATE_PATH = ".claude-flow/data/classic-loop.json";
488
+ var LEDGER_PATH = ".claude-flow/data/verified-ingest.json";
489
+ var MAX_READ = 8 * 1024 * 1024;
490
+ var MAX_LESSONS_FILE = 4 * 1024 * 1024;
491
+ var MAX_SESSIONS = 50;
492
+ var SESSION2 = /^[A-Za-z0-9_-]{1,64}$/;
493
+ var OFF = /^(0|false|off|no)$/i;
494
+ function enabled(root, env = process.env) {
495
+ if (OFF.test(env.RUFLO_LEARNING_LOOP ?? "")) return false;
496
+ if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(root, ".claude-flow"))) return false;
497
+ try {
498
+ const config = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(root, ".claude-flow", "config.json"), "utf8"));
499
+ if (config?.learningLoop?.enabled === false) return false;
500
+ } catch {
501
+ }
502
+ return true;
503
+ }
504
+ function readState(root) {
505
+ try {
506
+ const raw = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(root, STATE_PATH), "utf8"));
507
+ if (raw && raw.v === 1 && raw.sessions && typeof raw.sessions === "object") return raw;
508
+ } catch {
509
+ }
510
+ return { v: 1, sessions: {} };
511
+ }
512
+ function writeAtomic(path, text) {
513
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
514
+ const tmp = `${path}.${process.pid}.tmp`;
515
+ (0, import_node_fs.writeFileSync)(tmp, text);
516
+ (0, import_node_fs.renameSync)(tmp, path);
517
+ }
518
+ function readRange(path, from, to) {
519
+ const fd = (0, import_node_fs.openSync)(path, "r");
520
+ try {
521
+ const buf = Buffer.alloc(to - from);
522
+ let got = 0;
523
+ while (got < buf.length) {
524
+ const n = (0, import_node_fs.readSync)(fd, buf, got, buf.length - got, from + got);
525
+ if (n === 0) break;
526
+ got += n;
527
+ }
528
+ return buf.subarray(0, got).toString("utf8");
529
+ } finally {
530
+ (0, import_node_fs.closeSync)(fd);
531
+ }
532
+ }
533
+ function recordTurns(opts) {
534
+ try {
535
+ const { root } = opts;
536
+ if (typeof opts.transcriptPath !== "string" || typeof opts.session !== "string" || !SESSION2.test(opts.session)) return 0;
537
+ if (!opts.transcriptPath.endsWith(".jsonl")) return 0;
538
+ const size = (0, import_node_fs.statSync)(opts.transcriptPath).size;
539
+ const state = readState(root);
540
+ const known = state.sessions[opts.session];
541
+ let from = known && known.offset <= size ? known.offset : 0;
542
+ if (size - from > MAX_READ) from = size - MAX_READ;
543
+ if (from >= size) return 0;
544
+ const text = readRange(opts.transcriptPath, from, size);
545
+ const now = opts.now ?? Date.now();
546
+ const { lines, consumed } = replay(text, { base: from, session: opts.session, root, final: opts.final, agentFor: opts.agentFor, now });
547
+ if (lines.length > 0) {
548
+ const path = (0, import_node_path.join)(root, VERIFIED_PATH);
549
+ let existing = "";
550
+ try {
551
+ existing = (0, import_node_fs.readFileSync)(path, "utf8");
552
+ } catch (error) {
553
+ if (error.code !== "ENOENT") return 0;
554
+ }
555
+ writeAtomic(path, appendLines(existing, lines));
556
+ }
557
+ state.sessions[opts.session] = { offset: from + consumed, at: now };
558
+ const keep = Object.entries(state.sessions).sort((a, b) => b[1].at - a[1].at).slice(0, MAX_SESSIONS);
559
+ writeAtomic((0, import_node_path.join)(root, STATE_PATH), JSON.stringify({ v: 1, sessions: Object.fromEntries(keep) }));
560
+ return lines.length;
561
+ } catch {
562
+ return 0;
563
+ }
564
+ }
565
+ function lessonsForPrompt(root, prompt) {
566
+ try {
567
+ if (typeof prompt !== "string" || prompt.trim() === "" || prompt.trimStart().startsWith("/")) return "";
568
+ const path = (0, import_node_path.join)(root, LESSONS_PATH);
569
+ if ((0, import_node_fs.statSync)(path).size > MAX_LESSONS_FILE) return "";
570
+ return lessonsFor(prompt, (0, import_node_fs.readFileSync)(path, "utf8"));
571
+ } catch {
572
+ return "";
573
+ }
574
+ }
575
+ function ingestDue(root) {
576
+ try {
577
+ const recorded = (0, import_node_fs.statSync)((0, import_node_path.join)(root, VERIFIED_PATH)).mtimeMs;
578
+ try {
579
+ return recorded > (0, import_node_fs.statSync)((0, import_node_path.join)(root, LEDGER_PATH)).mtimeMs;
580
+ } catch {
581
+ return true;
582
+ }
583
+ } catch {
584
+ return false;
585
+ }
586
+ }
587
+ // Annotate the CommonJS export names for ESM import in node:
588
+ 0 && (module.exports = {
589
+ MAX_READ,
590
+ enabled,
591
+ ingestDue,
592
+ lessonsForPrompt,
593
+ recordTurns
594
+ });
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-30T19:15:36.709Z",
5
- "gitSha": "5ccd8011",
4
+ "generatedAt": "2026-10-31T05:51:35.623Z",
5
+ "gitSha": "0d94037e",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 445,
@@ -4656,6 +4656,7 @@ const verifiedIngestCommand = {
4656
4656
  options: [
4657
4657
  { name: 'dry-run', description: 'Count only: train nothing and write nothing', type: 'boolean', default: false },
4658
4658
  { name: 'force', description: 'Re-read the file even if unchanged since the last ingest', type: 'boolean', default: false },
4659
+ { name: 'quiet', description: 'Suppress output (used when spawned detached from a hook)', type: 'boolean', default: false },
4659
4660
  ],
4660
4661
  examples: [
4661
4662
  { command: 'ruflo hooks verified-ingest', description: 'Ingest new verified outcomes and refresh learned-lessons.json' },
@@ -4664,12 +4665,14 @@ const verifiedIngestCommand = {
4664
4665
  action: async (ctx) => {
4665
4666
  try {
4666
4667
  const result = await callMCPTool('hooks_intelligence_verified-ingest', { dryRun: ctx.flags['dry-run'] === true || ctx.flags.dryRun === true, force: ctx.flags.force === true });
4668
+ if (ctx.flags.quiet)
4669
+ return { success: true, data: result };
4667
4670
  if (ctx.flags.format === 'json') {
4668
4671
  output.printJson(result);
4669
4672
  return { success: true, data: result };
4670
4673
  }
4671
4674
  if (result.skipped === 'no-file') {
4672
- output.printInfo('No recorded turns yet (.claude-flow/data/verified-trajectories.jsonl). Turn on the ruflo-mods learningLoop option.');
4675
+ output.printInfo('No recorded turns yet (.claude-flow/data/verified-trajectories.jsonl). The classic hooks and the ruflo-mods learningLoop option both record them.');
4673
4676
  return { success: true, data: result };
4674
4677
  }
4675
4678
  if (!result.sonaAvailable)
@@ -540,7 +540,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false, riskySe
540
540
  const sourceHelpersForUpgrade = findSourceHelpersDir();
541
541
  if (sourceHelpersForUpgrade) {
542
542
  // Keep in sync with helper-refresh.ts:CRITICAL_HELPERS.
543
- const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.cjs'];
543
+ const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.cjs', 'learning-loop.cjs'];
544
544
  for (const helperName of criticalHelpers) {
545
545
  const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
546
546
  const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
@@ -78,6 +78,10 @@ export const CRITICAL_HELPERS = [
78
78
  // ("latest" -> tester). ADR-389 / #3401. It is CommonJS, so it ships as
79
79
  // `.cjs`: a `.js` copy cannot load in a `"type":"module"` project (#3555).
80
80
  'router.cjs',
81
+ // learning-loop.cjs is the classic-hooks learning loop hook-handler.cjs loads (ADR-512),
82
+ // bundled from the ruflo-mods loop source; here so every install records verified turns
83
+ // and sees lessons on its next `ruflo` command, not only with the mod.
84
+ 'learning-loop.cjs',
81
85
  ];
82
86
  function errorCode(error) {
83
87
  return typeof error === 'object' && error !== null && 'code' in error