agentsmesh 0.40.0 → 0.41.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.
- package/CHANGELOG.md +75 -0
- package/README.md +1 -1
- package/dist/canonical.js +299 -238
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +271 -332
- package/dist/engine.d.ts +7 -8
- package/dist/engine.js +771 -276
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1733 -521
- package/dist/index.js.map +1 -1
- package/dist/{init-B1qdo3Dl.d.ts → init-DruMEhnc.d.ts} +97 -42
- package/dist/lessons.d.ts +7 -4
- package/dist/lessons.js +2091 -613
- package/dist/lessons.js.map +1 -1
- package/dist/{target-descriptor-CvTJyDxd.d.ts → target-descriptor-CMV6vxVE.d.ts} +7 -0
- package/dist/targets.d.ts +2 -2
- package/dist/targets.js +265 -219
- package/dist/targets.js.map +1 -1
- package/package.json +3 -2
package/dist/lessons.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createHash, randomUUID } from 'crypto';
|
|
3
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, realpathSync, appendFileSync } from 'fs';
|
|
4
|
-
import { resolve,
|
|
5
|
-
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
3
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, chmodSync, realpathSync, accessSync, constants, appendFileSync, rmdirSync, unlinkSync, openSync, readSync, closeSync } from 'fs';
|
|
4
|
+
import { resolve, join, relative, sep, dirname, posix, basename, extname } from 'path';
|
|
6
5
|
import picomatch from 'picomatch';
|
|
7
|
-
import {
|
|
6
|
+
import { execFile, spawnSync } from 'child_process';
|
|
7
|
+
import { hostname, tmpdir } from 'os';
|
|
8
|
+
import { mkdir, writeFile, rm, readFile, lstat, open, readdir, rmdir, unlink, stat, rename, realpath } from 'fs/promises';
|
|
8
9
|
import { setTimeout } from 'timers/promises';
|
|
9
|
-
import {
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
10
12
|
|
|
11
13
|
// src/lessons/graph-schema.ts
|
|
12
14
|
var CURRENT_GRAPH_VERSION = 2;
|
|
@@ -52,22 +54,320 @@ var LessonsGraphSchema = z.object({
|
|
|
52
54
|
function parseGraph(raw) {
|
|
53
55
|
return LessonsGraphSchema.parse(raw);
|
|
54
56
|
}
|
|
57
|
+
function emptyGraph() {
|
|
58
|
+
return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/lessons/glob-expand.ts
|
|
62
|
+
var MARK = "\0";
|
|
63
|
+
var LED = "";
|
|
64
|
+
var MAX_EXPANSIONS = 64;
|
|
65
|
+
function fail(reason) {
|
|
66
|
+
throw new Error(reason);
|
|
67
|
+
}
|
|
68
|
+
function skipClass(s, i) {
|
|
69
|
+
const end = s.indexOf("]", i + 1);
|
|
70
|
+
return end === -1 ? i + 1 : end + 1;
|
|
71
|
+
}
|
|
72
|
+
function markLedStars(body) {
|
|
73
|
+
let out2 = "";
|
|
74
|
+
let depth = 0;
|
|
75
|
+
for (let i = 0; i < body.length; ) {
|
|
76
|
+
const c2 = body[i];
|
|
77
|
+
if (c2 === "[") {
|
|
78
|
+
const end = skipClass(body, i);
|
|
79
|
+
out2 += body.slice(i, end);
|
|
80
|
+
i = end;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c2 === "{") depth += 1;
|
|
84
|
+
if (c2 === "}" && depth > 0) depth -= 1;
|
|
85
|
+
const prev = body[i - 1];
|
|
86
|
+
if (c2 === "*" && prev === "." && depth > 0) fail(".* inside {\u2026} is not supported");
|
|
87
|
+
const segmentStart = i === 0 || prev === "/";
|
|
88
|
+
const afterLeadingDot = prev === "." && (i === 1 || body[i - 2] === "/");
|
|
89
|
+
const led = c2 === "*" && body[i + 1] !== "*" && prev !== "*" && (segmentStart || afterLeadingDot);
|
|
90
|
+
out2 += led ? LED : c2;
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
return out2;
|
|
94
|
+
}
|
|
95
|
+
function expandBraces(s) {
|
|
96
|
+
let open2 = -1;
|
|
97
|
+
for (let i = 0; i < s.length && open2 === -1; ) {
|
|
98
|
+
if (s[i] === "[") i = skipClass(s, i);
|
|
99
|
+
else if (s[i] === "}") fail("unbalanced }");
|
|
100
|
+
else if (s[i] === "{") open2 = i;
|
|
101
|
+
else i += 1;
|
|
102
|
+
}
|
|
103
|
+
if (open2 === -1) return [s];
|
|
104
|
+
const options = [];
|
|
105
|
+
let depth = 0;
|
|
106
|
+
let start = open2 + 1;
|
|
107
|
+
let close = -1;
|
|
108
|
+
for (let i = open2 + 1; i < s.length && close === -1; ) {
|
|
109
|
+
const c2 = s[i];
|
|
110
|
+
if (c2 === "[") {
|
|
111
|
+
i = skipClass(s, i);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c2 === "{") depth += 1;
|
|
115
|
+
else if (c2 === "}" && depth > 0) depth -= 1;
|
|
116
|
+
else if (c2 === "}") close = i;
|
|
117
|
+
else if (c2 === "," && depth === 0) {
|
|
118
|
+
options.push(s.slice(start, i));
|
|
119
|
+
start = i + 1;
|
|
120
|
+
}
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
if (close === -1) fail("unclosed {");
|
|
124
|
+
if (options.length === 0)
|
|
125
|
+
fail("brace groups need a comma, e.g. {a,b} (ranges are not supported)");
|
|
126
|
+
options.push(s.slice(start, close));
|
|
127
|
+
const suffixes = expandBraces(s.slice(close + 1));
|
|
128
|
+
const out2 = [];
|
|
129
|
+
for (const option of options) {
|
|
130
|
+
for (const head of expandBraces(option)) {
|
|
131
|
+
for (const tail of suffixes) {
|
|
132
|
+
out2.push(s.slice(0, open2) + MARK + head + MARK + tail);
|
|
133
|
+
if (out2.length > MAX_EXPANSIONS) fail(`more than ${MAX_EXPANSIONS} brace expansions`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out2;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/lessons/glob-parse.ts
|
|
141
|
+
var MAX_GLOB_LENGTH = 256;
|
|
142
|
+
function parseGlob(pattern) {
|
|
143
|
+
try {
|
|
144
|
+
return parseOrThrow(pattern);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return err instanceof Error ? err.message : String(err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function parseOrThrow(pattern) {
|
|
150
|
+
if (pattern.length > MAX_GLOB_LENGTH) fail(`longer than ${MAX_GLOB_LENGTH} characters`);
|
|
151
|
+
if (pattern.includes("\\")) fail("backslash escapes are not supported (use / as separator)");
|
|
152
|
+
if (/["\u0000-\u001f]/.test(pattern)) fail("quotes and control characters are not supported");
|
|
153
|
+
if (/[()|]/.test(pattern.replace(/\[[^\]/]*\]/g, ""))) {
|
|
154
|
+
fail("extglobs and (\u2026)/| groups are not supported (use {a,b})");
|
|
155
|
+
}
|
|
156
|
+
if (/[[\]{}]\+/.test(pattern)) fail("+ after a class or brace is a regex quantifier");
|
|
157
|
+
const negated = pattern.startsWith("!");
|
|
158
|
+
let body = negated ? pattern.slice(1) : pattern;
|
|
159
|
+
if (body.startsWith("./")) body = body.slice(2);
|
|
160
|
+
if (body.startsWith("!") || body.startsWith("./")) fail("use a single leading ! and ./");
|
|
161
|
+
if (body === "") fail("empty pattern");
|
|
162
|
+
const fastPath = !negated && (body === "*.*" || body === "**/*.*");
|
|
163
|
+
if (fastPath) body = `${body.slice(0, -1)}?*`;
|
|
164
|
+
return { negated, alternatives: expandBraces(markLedStars(body)).map(parseAlternative) };
|
|
165
|
+
}
|
|
166
|
+
function parseAlternative(alt) {
|
|
167
|
+
const raw = alt.split("/");
|
|
168
|
+
const out2 = [];
|
|
169
|
+
raw.forEach((segment, i) => {
|
|
170
|
+
if (segment.includes("**")) {
|
|
171
|
+
if (segment !== "**") fail("** must be a whole segment, outside {\u2026} (use * in a segment)");
|
|
172
|
+
if (out2.at(-1)?.k === "globstar") return;
|
|
173
|
+
const before = raw[i - 1] ?? "";
|
|
174
|
+
const trailing = raw.slice(i + 1).every((s) => s === "**");
|
|
175
|
+
const min1 = trailing && (before.endsWith("*") || before.endsWith(LED));
|
|
176
|
+
out2.push({ k: "globstar", min1 });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const tokens = tokenize(segment, alt);
|
|
180
|
+
const guarded = segment.includes(LED);
|
|
181
|
+
const literal = tokens.every((t) => t.k === "lit") ? tokens.map((t) => t.k === "lit" ? t.ch : "").join("") : null;
|
|
182
|
+
const matchesEmpty = segment !== "" && !guarded && tokens.every((t) => t.k === "star");
|
|
183
|
+
out2.push({ k: "segment", tokens, literal, guarded, matchesEmpty });
|
|
184
|
+
});
|
|
185
|
+
return out2;
|
|
186
|
+
}
|
|
187
|
+
function tokenize(segment, alt) {
|
|
188
|
+
const tokens = [];
|
|
189
|
+
for (let i = 0; i < segment.length; i += 1) {
|
|
190
|
+
const c2 = segment[i];
|
|
191
|
+
if (c2 === MARK) continue;
|
|
192
|
+
if (c2 === "*" || c2 === LED) tokens.push({ k: "star" });
|
|
193
|
+
else if (c2 === "?") tokens.push({ k: "one" });
|
|
194
|
+
else if (c2 === "[") {
|
|
195
|
+
const end = segment.indexOf("]", i + 1);
|
|
196
|
+
if (end === -1) {
|
|
197
|
+
if (alt.includes("]")) fail("a [...] class cannot span /");
|
|
198
|
+
tokens.push({ k: "lit", ch: c2 });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
tokens.push(parseClass(segment.slice(i + 1, end)));
|
|
202
|
+
i = end;
|
|
203
|
+
} else tokens.push({ k: "lit", ch: c2 });
|
|
204
|
+
}
|
|
205
|
+
return tokens;
|
|
206
|
+
}
|
|
207
|
+
var CLASS_SPECIAL = /[-*+?.^${}(|)[\]]/;
|
|
208
|
+
function parseClass(body) {
|
|
209
|
+
if (body === "") fail("empty [] class");
|
|
210
|
+
if (body.startsWith("!")) fail("[!...] is not a negation here; use [^...]");
|
|
211
|
+
if (body.includes("[")) fail("POSIX [:classes:] and nested [ are not supported");
|
|
212
|
+
const negated = body.startsWith("^");
|
|
213
|
+
const members = negated ? body.slice(1) : body;
|
|
214
|
+
if (members === "") fail("empty [^] class");
|
|
215
|
+
const ranges = [];
|
|
216
|
+
for (let i = 0; i < members.length; i += 1) {
|
|
217
|
+
const lo = members[i];
|
|
218
|
+
const hi = members[i + 2];
|
|
219
|
+
if (members[i + 1] === "-" && hi !== void 0) {
|
|
220
|
+
if (hi < lo) fail(`class range out of order: ${lo}-${hi}`);
|
|
221
|
+
ranges.push([lo, hi]);
|
|
222
|
+
i += 2;
|
|
223
|
+
} else ranges.push([lo, lo]);
|
|
224
|
+
}
|
|
225
|
+
const inSet = (c2) => ranges.some(([lo, hi]) => c2 >= lo && c2 <= hi);
|
|
226
|
+
const literal = CLASS_SPECIAL.test(body) ? null : `[${body}]`;
|
|
227
|
+
return { k: "class", test: negated ? (c2) => c2 !== "/" && !inSet(c2) : inSet, literal };
|
|
228
|
+
}
|
|
229
|
+
function normalizeRecallFile(file, projectRoot) {
|
|
230
|
+
const forward = file.replaceAll("\\", "/");
|
|
231
|
+
const direct = relativize(projectRoot, forward);
|
|
232
|
+
if (!direct.startsWith("../")) return direct;
|
|
233
|
+
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
234
|
+
return viaReal.startsWith("../") ? direct : viaReal;
|
|
235
|
+
}
|
|
236
|
+
function relativize(root, forward) {
|
|
237
|
+
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
238
|
+
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
239
|
+
}
|
|
240
|
+
function safeRealpath(path) {
|
|
241
|
+
try {
|
|
242
|
+
return realpathSync(path);
|
|
243
|
+
} catch {
|
|
244
|
+
const parent = dirname(path);
|
|
245
|
+
if (parent === path) return path;
|
|
246
|
+
return resolve(safeRealpath(parent), basename(path));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/lessons/trigger-file-glob.ts
|
|
251
|
+
var ABSOLUTE = /^(?:[A-Za-z]:)?\//;
|
|
252
|
+
var GLOB_CHARS = /[*?[{]/;
|
|
253
|
+
var CODES = {
|
|
254
|
+
outside: "TRIGGER_FILE_OUTSIDE_PROJECT",
|
|
255
|
+
root: "TRIGGER_FILE_IS_PROJECT_ROOT",
|
|
256
|
+
folder: "TRIGGER_FILE_IS_DIRECTORY",
|
|
257
|
+
unsafe: "UNSAFE_GLOB_PATTERN"
|
|
258
|
+
};
|
|
259
|
+
function problemMessage(given, problem, detail) {
|
|
260
|
+
switch (problem) {
|
|
261
|
+
case "outside":
|
|
262
|
+
return `--trigger-file ${given} points outside the project root. File triggers match project-relative paths, so it would never fire \u2014 pass a glob relative to the project root (e.g. "src/**/*.ts").`;
|
|
263
|
+
case "root":
|
|
264
|
+
return `--trigger-file ${given} is the project root itself, and file triggers match files. Pass a glob such as "src/**/*.ts".`;
|
|
265
|
+
case "folder":
|
|
266
|
+
return `--trigger-file ${given} is a folder, and file triggers match files. Use ${JSON.stringify(`${detail}/**`)} to match every file in it.`;
|
|
267
|
+
case "unsafe":
|
|
268
|
+
return `--trigger-file ${given} is outside the safe glob subset: ${detail}. Use only *, **, ?, [...] and {a,b}.`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
var TriggerFileGlobError = class extends Error {
|
|
272
|
+
constructor(pattern, problem = "outside", detail = "") {
|
|
273
|
+
super(problemMessage(JSON.stringify(pattern), problem, detail));
|
|
274
|
+
this.pattern = pattern;
|
|
275
|
+
this.name = "TriggerFileGlobError";
|
|
276
|
+
this.code = CODES[problem];
|
|
277
|
+
}
|
|
278
|
+
code;
|
|
279
|
+
};
|
|
280
|
+
function sameFolder(a, b) {
|
|
281
|
+
try {
|
|
282
|
+
return realpathSync(a) === realpathSync(b);
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function isFolder(projectRoot, path) {
|
|
288
|
+
if (GLOB_CHARS.test(path)) return false;
|
|
289
|
+
return statSync(join(projectRoot, path), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
290
|
+
}
|
|
291
|
+
function projectRelativeGlob(pattern, projectRoot) {
|
|
292
|
+
const forward = pattern.trim().replaceAll("\\", "/");
|
|
293
|
+
let rel = forward;
|
|
294
|
+
if (ABSOLUTE.test(forward)) {
|
|
295
|
+
const root = projectRoot.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
296
|
+
rel = forward.startsWith(`${root}/`) || forward === root ? forward.slice(root.length + 1) : normalizeRecallFile(forward, projectRoot);
|
|
297
|
+
if (ABSOLUTE.test(rel)) {
|
|
298
|
+
throw new TriggerFileGlobError(
|
|
299
|
+
pattern,
|
|
300
|
+
sameFolder(forward, projectRoot) ? "root" : "outside"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const normalized = posix.normalize(rel === "" ? "." : rel);
|
|
305
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
306
|
+
throw new TriggerFileGlobError(pattern, "outside");
|
|
307
|
+
}
|
|
308
|
+
const path = normalized.replace(/\/+$/, "");
|
|
309
|
+
if (path === "." || path === "") throw new TriggerFileGlobError(pattern, "root");
|
|
310
|
+
if (path !== normalized || isFolder(projectRoot, path)) {
|
|
311
|
+
throw new TriggerFileGlobError(pattern, "folder", path);
|
|
312
|
+
}
|
|
313
|
+
const unsafe = parseGlob(path);
|
|
314
|
+
if (typeof unsafe === "string") throw new TriggerFileGlobError(pattern, "unsafe", unsafe);
|
|
315
|
+
return path;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/lessons/add-helpers.ts
|
|
55
319
|
function normalizeRule(rule) {
|
|
56
320
|
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
57
321
|
}
|
|
58
322
|
function union(base, extra) {
|
|
59
|
-
const
|
|
60
|
-
for (const item of extra) if (!
|
|
61
|
-
return
|
|
323
|
+
const out2 = [...base];
|
|
324
|
+
for (const item of extra) if (!out2.includes(item)) out2.push(item);
|
|
325
|
+
return out2;
|
|
326
|
+
}
|
|
327
|
+
function upsertLesson(before, input, triggerIds) {
|
|
328
|
+
return {
|
|
329
|
+
...before,
|
|
330
|
+
topics: union(before.topics, [input.topic]),
|
|
331
|
+
triggers: union(before.triggers, triggerIds),
|
|
332
|
+
evidence: union(before.evidence, input.evidence ?? []),
|
|
333
|
+
...before.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
|
|
334
|
+
...input.scope === "always" ? { scope: "always" } : {}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function describeUpsert(before, after) {
|
|
338
|
+
const added = (base, next) => next.filter((item) => !base.includes(item));
|
|
339
|
+
const topics = added(before.topics, after.topics);
|
|
340
|
+
const triggers = added(before.triggers, after.triggers);
|
|
341
|
+
const evidence = added(before.evidence, after.evidence);
|
|
342
|
+
const changes = [];
|
|
343
|
+
if (after.scope === "always" && before.scope !== "always") changes.push("scope set to always");
|
|
344
|
+
if (topics.length > 0) changes.push(`topic added: ${topics.join(", ")}`);
|
|
345
|
+
if (triggers.length > 0) {
|
|
346
|
+
changes.push(`trigger${triggers.length === 1 ? "" : "s"} attached: ${triggers.join(", ")}`);
|
|
347
|
+
}
|
|
348
|
+
if (evidence.length > 0) changes.push(`evidence added: ${evidence.join(", ")}`);
|
|
349
|
+
if (before.rationale === void 0 && after.rationale !== void 0) {
|
|
350
|
+
changes.push("rationale added");
|
|
351
|
+
}
|
|
352
|
+
return changes;
|
|
62
353
|
}
|
|
63
|
-
function
|
|
354
|
+
function findExistingLessonByRule(graph, ruleKey) {
|
|
355
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
356
|
+
if (lesson.status !== "active") continue;
|
|
357
|
+
if (normalizeRule(lesson.rule) === ruleKey) return id;
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
function mergeTriggers(graph, spec, projectRoot) {
|
|
64
362
|
const requested = [
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// a backslash pattern dedupe against the forward-slash node it equals.
|
|
363
|
+
// Recall matches forward-slash, project-relative paths (normalizeRecallFile),
|
|
364
|
+
// so a backslash or absolute pattern stored raw would silently never fire.
|
|
365
|
+
// Normalizing here also dedupes it against the node it equals.
|
|
69
366
|
...(spec.files ?? []).map(
|
|
70
|
-
(p) => ({
|
|
367
|
+
(p) => ({
|
|
368
|
+
kind: "file_glob",
|
|
369
|
+
pattern: projectRoot === void 0 ? p.replaceAll("\\", "/") : projectRelativeGlob(p, projectRoot)
|
|
370
|
+
})
|
|
71
371
|
),
|
|
72
372
|
...(spec.commands ?? []).map((p) => ({ kind: "command_pattern", pattern: p })),
|
|
73
373
|
...(spec.keywords ?? []).map((p) => ({ kind: "keyword", pattern: p }))
|
|
@@ -123,13 +423,156 @@ function ruleToSlug(rule) {
|
|
|
123
423
|
function todayIso() {
|
|
124
424
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
125
425
|
}
|
|
126
|
-
var
|
|
426
|
+
var UTF8_BOM = "\uFEFF";
|
|
427
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
428
|
+
".md",
|
|
429
|
+
".mdc",
|
|
430
|
+
".mdx",
|
|
431
|
+
".markdown",
|
|
432
|
+
".txt",
|
|
433
|
+
".json",
|
|
434
|
+
".jsonc",
|
|
435
|
+
".yaml",
|
|
436
|
+
".yml",
|
|
437
|
+
".toml",
|
|
438
|
+
".ini",
|
|
439
|
+
".sh",
|
|
440
|
+
".bash",
|
|
441
|
+
".zsh",
|
|
442
|
+
".ps1",
|
|
443
|
+
".js",
|
|
444
|
+
".mjs",
|
|
445
|
+
".cjs",
|
|
446
|
+
".ts",
|
|
447
|
+
".tsx",
|
|
448
|
+
".html",
|
|
449
|
+
".css"
|
|
450
|
+
]);
|
|
451
|
+
var TEXT_DOTFILES = /* @__PURE__ */ new Set([
|
|
452
|
+
".gitignore",
|
|
453
|
+
".cursorignore",
|
|
454
|
+
".cursorindexingignore",
|
|
455
|
+
".aiignore",
|
|
456
|
+
".agentignore",
|
|
457
|
+
".clineignore",
|
|
458
|
+
".geminiignore",
|
|
459
|
+
".codeiumignore",
|
|
460
|
+
".continueignore",
|
|
461
|
+
".copilotignore",
|
|
462
|
+
".windsurfignore",
|
|
463
|
+
".junieignore",
|
|
464
|
+
".kiroignore",
|
|
465
|
+
".rooignore",
|
|
466
|
+
".antigravityignore"
|
|
467
|
+
]);
|
|
468
|
+
function shouldNormalizeLineEndings(path) {
|
|
469
|
+
const ext = extname(path).toLowerCase();
|
|
470
|
+
if (ext.length > 0) return TEXT_EXTENSIONS.has(ext);
|
|
471
|
+
const base = basename(path).toLowerCase();
|
|
472
|
+
return TEXT_DOTFILES.has(base);
|
|
473
|
+
}
|
|
474
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
475
|
+
".png",
|
|
476
|
+
".jpg",
|
|
477
|
+
".jpeg",
|
|
478
|
+
".gif",
|
|
479
|
+
".bmp",
|
|
480
|
+
".ico",
|
|
481
|
+
".webp",
|
|
482
|
+
".avif",
|
|
483
|
+
".tiff",
|
|
484
|
+
".pdf",
|
|
485
|
+
".zip",
|
|
486
|
+
".gz",
|
|
487
|
+
".tgz",
|
|
488
|
+
".bz2",
|
|
489
|
+
".xz",
|
|
490
|
+
".7z",
|
|
491
|
+
".rar",
|
|
492
|
+
".jar",
|
|
493
|
+
".woff",
|
|
494
|
+
".woff2",
|
|
495
|
+
".ttf",
|
|
496
|
+
".otf",
|
|
497
|
+
".eot",
|
|
498
|
+
".mp3",
|
|
499
|
+
".mp4",
|
|
500
|
+
".wav",
|
|
501
|
+
".ogg",
|
|
502
|
+
".webm",
|
|
503
|
+
".mov",
|
|
504
|
+
".wasm",
|
|
505
|
+
".bin",
|
|
506
|
+
".dat",
|
|
507
|
+
".db",
|
|
508
|
+
".sqlite",
|
|
509
|
+
".so",
|
|
510
|
+
".dylib",
|
|
511
|
+
".dll",
|
|
512
|
+
".exe",
|
|
513
|
+
".class",
|
|
514
|
+
".pyc",
|
|
515
|
+
// Office and design documents are zip or proprietary containers.
|
|
516
|
+
".xlsx",
|
|
517
|
+
".xls",
|
|
518
|
+
".docx",
|
|
519
|
+
".doc",
|
|
520
|
+
".pptx",
|
|
521
|
+
".ppt",
|
|
522
|
+
".odt",
|
|
523
|
+
".ods",
|
|
524
|
+
".psd",
|
|
525
|
+
".ai",
|
|
526
|
+
".sketch",
|
|
527
|
+
".fig",
|
|
528
|
+
".heic",
|
|
529
|
+
".heif",
|
|
530
|
+
// Archives, columnar data, models and compressed payloads.
|
|
531
|
+
".tar",
|
|
532
|
+
".zst",
|
|
533
|
+
".br",
|
|
534
|
+
".lz4",
|
|
535
|
+
".parquet",
|
|
536
|
+
".avro",
|
|
537
|
+
".orc",
|
|
538
|
+
".pkl",
|
|
539
|
+
".npy",
|
|
540
|
+
".npz",
|
|
541
|
+
".onnx",
|
|
542
|
+
".pt",
|
|
543
|
+
".safetensors",
|
|
544
|
+
".gguf",
|
|
545
|
+
".sqlite3",
|
|
546
|
+
".avi",
|
|
547
|
+
".mkv",
|
|
548
|
+
".flac",
|
|
549
|
+
".aac",
|
|
550
|
+
".m4a",
|
|
551
|
+
".ttc"
|
|
552
|
+
]);
|
|
553
|
+
function isBinaryPayloadPath(path) {
|
|
554
|
+
return BINARY_EXTENSIONS.has(extname(path).toLowerCase());
|
|
555
|
+
}
|
|
556
|
+
function payloadEncodingFor(path) {
|
|
557
|
+
return isBinaryPayloadPath(path) ? "latin1" : "utf-8";
|
|
558
|
+
}
|
|
559
|
+
function normalizeLineEndings(content) {
|
|
560
|
+
return content.replace(/\r\n?/g, "\n");
|
|
561
|
+
}
|
|
562
|
+
var EXECUTABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh"]);
|
|
563
|
+
function executableModeFor(path) {
|
|
564
|
+
return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
|
|
565
|
+
}
|
|
566
|
+
function stripBom(text) {
|
|
567
|
+
return text.startsWith(UTF8_BOM) ? text.slice(UTF8_BOM.length) : text;
|
|
568
|
+
}
|
|
569
|
+
var LESSONS_GRAPH_PATH = ".agentsmesh/lessons/lessons.json";
|
|
127
570
|
function graphFilePath(projectRoot) {
|
|
128
|
-
return resolve(projectRoot,
|
|
571
|
+
return resolve(projectRoot, LESSONS_GRAPH_PATH);
|
|
129
572
|
}
|
|
130
573
|
function loadLessonsGraph(projectRoot) {
|
|
131
574
|
const raw = readFileSync(graphFilePath(projectRoot), "utf8");
|
|
132
|
-
return parseGraph(JSON.parse(raw));
|
|
575
|
+
return parseGraph(JSON.parse(stripBom(raw)));
|
|
133
576
|
}
|
|
134
577
|
function tryLoadLessonsGraph(projectRoot) {
|
|
135
578
|
if (!existsSync(graphFilePath(projectRoot))) return null;
|
|
@@ -139,7 +582,7 @@ function loadLessonsGraphResilient(projectRoot) {
|
|
|
139
582
|
const path = graphFilePath(projectRoot);
|
|
140
583
|
if (!existsSync(path)) return { status: "absent", graph: null };
|
|
141
584
|
try {
|
|
142
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
585
|
+
const parsed = JSON.parse(stripBom(readFileSync(path, "utf8")));
|
|
143
586
|
const version = parsed?.version;
|
|
144
587
|
if (typeof version === "number" && version > CURRENT_GRAPH_VERSION) {
|
|
145
588
|
return { status: "newer-version", graph: null, version };
|
|
@@ -153,11 +596,37 @@ function loadLessonsGraphResilient(projectRoot) {
|
|
|
153
596
|
};
|
|
154
597
|
}
|
|
155
598
|
}
|
|
599
|
+
var LessonsGraphReadOnlyError = class extends Error {
|
|
600
|
+
constructor() {
|
|
601
|
+
super(
|
|
602
|
+
`${LESSONS_GRAPH_PATH} is read-only, so nothing was saved. Make it writable (chmod u+w ${LESSONS_GRAPH_PATH}) to change lessons.`
|
|
603
|
+
);
|
|
604
|
+
this.name = "LessonsGraphReadOnlyError";
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
function fileMode(path) {
|
|
608
|
+
try {
|
|
609
|
+
return statSync(path).mode & 511;
|
|
610
|
+
} catch {
|
|
611
|
+
return void 0;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function isWritable(path) {
|
|
615
|
+
try {
|
|
616
|
+
accessSync(path, constants.W_OK);
|
|
617
|
+
return true;
|
|
618
|
+
} catch {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
156
622
|
function saveLessonsGraph(projectRoot, graph) {
|
|
157
623
|
const path = graphFilePath(projectRoot);
|
|
158
624
|
mkdirSync(dirname(path), { recursive: true });
|
|
625
|
+
const mode = fileMode(path);
|
|
626
|
+
if (mode !== void 0 && !isWritable(path)) throw new LessonsGraphReadOnlyError();
|
|
159
627
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
160
628
|
writeFileSync(tmp, serializeGraph(graph), "utf8");
|
|
629
|
+
if (mode !== void 0) chmodSync(tmp, mode);
|
|
161
630
|
renameSync(tmp, path);
|
|
162
631
|
}
|
|
163
632
|
function serializeGraph(graph) {
|
|
@@ -171,9 +640,9 @@ function canonicalize(value) {
|
|
|
171
640
|
const entries = Object.entries(value).sort(
|
|
172
641
|
([a], [b]) => a < b ? -1 : 1
|
|
173
642
|
);
|
|
174
|
-
const
|
|
175
|
-
for (const [k, v] of entries)
|
|
176
|
-
return
|
|
643
|
+
const out2 = {};
|
|
644
|
+
for (const [k, v] of entries) out2[k] = canonicalize(v);
|
|
645
|
+
return out2;
|
|
177
646
|
}
|
|
178
647
|
return value;
|
|
179
648
|
}
|
|
@@ -301,6 +770,27 @@ var UnknownTopicError = class extends Error {
|
|
|
301
770
|
}
|
|
302
771
|
code = "UNKNOWN_TOPIC";
|
|
303
772
|
};
|
|
773
|
+
var InvalidTopicIdError = class extends Error {
|
|
774
|
+
constructor(topic) {
|
|
775
|
+
const slug = topic.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
776
|
+
super(
|
|
777
|
+
`Topic id ${JSON.stringify(topic)} must be kebab-case (lowercase letters, digits and -)` + (slug.length > 0 ? `, e.g. ${JSON.stringify(slug)}.` : ".")
|
|
778
|
+
);
|
|
779
|
+
this.topic = topic;
|
|
780
|
+
this.name = "InvalidTopicIdError";
|
|
781
|
+
}
|
|
782
|
+
code = "INVALID_TOPIC_ID";
|
|
783
|
+
};
|
|
784
|
+
var TopicSummaryRequiredError = class extends Error {
|
|
785
|
+
constructor(topic) {
|
|
786
|
+
super(
|
|
787
|
+
`New topic ${JSON.stringify(topic)} needs a one-line summary (--topic-summary on the CLI, topic_summary over MCP).`
|
|
788
|
+
);
|
|
789
|
+
this.topic = topic;
|
|
790
|
+
this.name = "TopicSummaryRequiredError";
|
|
791
|
+
}
|
|
792
|
+
code = "TOPIC_SUMMARY_REQUIRED";
|
|
793
|
+
};
|
|
304
794
|
var RuleTooLongError = class extends Error {
|
|
305
795
|
constructor(length, max) {
|
|
306
796
|
super(
|
|
@@ -344,8 +834,8 @@ var Builder = class {
|
|
|
344
834
|
return this.states.length - 1;
|
|
345
835
|
}
|
|
346
836
|
};
|
|
347
|
-
function isNonLineTerminator(
|
|
348
|
-
return
|
|
837
|
+
function isNonLineTerminator(c2) {
|
|
838
|
+
return c2 !== "\n" && c2 !== "\r" && c2 !== "\u2028" && c2 !== "\u2029";
|
|
349
839
|
}
|
|
350
840
|
function compileNode(b, node) {
|
|
351
841
|
switch (node.k) {
|
|
@@ -362,7 +852,7 @@ function compileNode(b, node) {
|
|
|
362
852
|
case "class": {
|
|
363
853
|
const s = b.alloc();
|
|
364
854
|
const e = b.alloc();
|
|
365
|
-
const test = node.k === "char" ? (
|
|
855
|
+
const test = node.k === "char" ? (c2) => c2 === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
|
|
366
856
|
b.states[s].chars.push({ test, target: e });
|
|
367
857
|
return { start: s, end: e };
|
|
368
858
|
}
|
|
@@ -490,7 +980,7 @@ var UnsupportedRegexError = class extends Error {
|
|
|
490
980
|
|
|
491
981
|
// src/lessons/regex-linear/parse-helpers.ts
|
|
492
982
|
var MAX_REPEAT = 1e3;
|
|
493
|
-
var isWord = (
|
|
983
|
+
var isWord = (c2) => /[A-Za-z0-9_]/.test(c2);
|
|
494
984
|
function expandRepeat(atom, min, max) {
|
|
495
985
|
const items = [];
|
|
496
986
|
for (let k = 0; k < min; k += 1) items.push(atom);
|
|
@@ -502,8 +992,8 @@ function expandRepeat(atom, min, max) {
|
|
|
502
992
|
if (items.length === 0) return { k: "empty" };
|
|
503
993
|
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
504
994
|
}
|
|
505
|
-
function escapeClass(
|
|
506
|
-
switch (
|
|
995
|
+
function escapeClass(c2) {
|
|
996
|
+
switch (c2) {
|
|
507
997
|
case "d":
|
|
508
998
|
return (x) => x >= "0" && x <= "9";
|
|
509
999
|
case "D":
|
|
@@ -522,8 +1012,11 @@ function escapeClass(c) {
|
|
|
522
1012
|
}
|
|
523
1013
|
var HEX2 = /^[0-9a-fA-F]{2}$/;
|
|
524
1014
|
var HEX4 = /^[0-9a-fA-F]{4}$/;
|
|
525
|
-
function readUnicodeEscape(src, i,
|
|
526
|
-
if (
|
|
1015
|
+
function readUnicodeEscape(src, i, c2) {
|
|
1016
|
+
if (c2 === "u" && src[i] === "{") {
|
|
1017
|
+
throw new UnsupportedRegexError("\\u{\u2026} code point escapes are not supported; use \\uHHHH");
|
|
1018
|
+
}
|
|
1019
|
+
if (c2 === "x") {
|
|
527
1020
|
const hex2 = src.slice(i, i + 2);
|
|
528
1021
|
return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
|
|
529
1022
|
}
|
|
@@ -543,8 +1036,8 @@ function classEscapeChar(src, i, e) {
|
|
|
543
1036
|
if (e === "x" || e === "u") return readUnicodeEscape(src, i, e);
|
|
544
1037
|
return { ch: escapeLiteral(e), len: 0 };
|
|
545
1038
|
}
|
|
546
|
-
function escapeLiteral(
|
|
547
|
-
switch (
|
|
1039
|
+
function escapeLiteral(c2) {
|
|
1040
|
+
switch (c2) {
|
|
548
1041
|
case "t":
|
|
549
1042
|
return " ";
|
|
550
1043
|
case "n":
|
|
@@ -558,7 +1051,7 @@ function escapeLiteral(c) {
|
|
|
558
1051
|
case "0":
|
|
559
1052
|
return "\0";
|
|
560
1053
|
default:
|
|
561
|
-
return
|
|
1054
|
+
return c2;
|
|
562
1055
|
}
|
|
563
1056
|
}
|
|
564
1057
|
|
|
@@ -610,27 +1103,27 @@ function parseRegex(src) {
|
|
|
610
1103
|
return { min, max };
|
|
611
1104
|
}
|
|
612
1105
|
function parseAtom() {
|
|
613
|
-
const
|
|
614
|
-
if (
|
|
615
|
-
if (
|
|
616
|
-
if (
|
|
617
|
-
if (
|
|
1106
|
+
const c2 = peek();
|
|
1107
|
+
if (c2 === "(") return parseGroup();
|
|
1108
|
+
if (c2 === "[") return parseClass2();
|
|
1109
|
+
if (c2 === "\\") return parseEscape();
|
|
1110
|
+
if (c2 === ".") {
|
|
618
1111
|
i += 1;
|
|
619
1112
|
return { k: "any" };
|
|
620
1113
|
}
|
|
621
|
-
if (
|
|
1114
|
+
if (c2 === "^") {
|
|
622
1115
|
i += 1;
|
|
623
1116
|
return { k: "assert", kind: "start" };
|
|
624
1117
|
}
|
|
625
|
-
if (
|
|
1118
|
+
if (c2 === "$") {
|
|
626
1119
|
i += 1;
|
|
627
1120
|
return { k: "assert", kind: "end" };
|
|
628
1121
|
}
|
|
629
|
-
if (
|
|
630
|
-
throw new UnsupportedRegexError(`Unexpected '${
|
|
1122
|
+
if (c2 === void 0 || c2 === "*" || c2 === "+" || c2 === "?" || c2 === ")") {
|
|
1123
|
+
throw new UnsupportedRegexError(`Unexpected '${c2 ?? "<end>"}' in pattern`);
|
|
631
1124
|
}
|
|
632
1125
|
i += 1;
|
|
633
|
-
return { k: "char", ch:
|
|
1126
|
+
return { k: "char", ch: c2 };
|
|
634
1127
|
}
|
|
635
1128
|
function parseGroup() {
|
|
636
1129
|
i += 1;
|
|
@@ -655,23 +1148,23 @@ function parseRegex(src) {
|
|
|
655
1148
|
}
|
|
656
1149
|
function parseEscape() {
|
|
657
1150
|
i += 1;
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
if (/[1-9]/.test(
|
|
1151
|
+
const c2 = peek();
|
|
1152
|
+
if (c2 === void 0) throw new UnsupportedRegexError("Trailing backslash");
|
|
1153
|
+
if (/[1-9]/.test(c2) || c2 === "k")
|
|
661
1154
|
throw new UnsupportedRegexError("Backreferences are not supported");
|
|
662
1155
|
i += 1;
|
|
663
|
-
if (
|
|
664
|
-
if (
|
|
665
|
-
const cls = escapeClass(
|
|
1156
|
+
if (c2 === "b") return { k: "assert", kind: "wordB" };
|
|
1157
|
+
if (c2 === "B") return { k: "assert", kind: "nonWordB" };
|
|
1158
|
+
const cls = escapeClass(c2);
|
|
666
1159
|
if (cls !== null) return { k: "class", test: cls };
|
|
667
|
-
if (
|
|
668
|
-
const { ch, len } =
|
|
1160
|
+
if (c2 === "x" || c2 === "u" || c2 === "c") {
|
|
1161
|
+
const { ch, len } = c2 === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c2);
|
|
669
1162
|
i += len;
|
|
670
1163
|
return { k: "char", ch };
|
|
671
1164
|
}
|
|
672
|
-
return { k: "char", ch: escapeLiteral(
|
|
1165
|
+
return { k: "char", ch: escapeLiteral(c2) };
|
|
673
1166
|
}
|
|
674
|
-
function
|
|
1167
|
+
function parseClass2() {
|
|
675
1168
|
i += 1;
|
|
676
1169
|
const negate = peek() === "^";
|
|
677
1170
|
if (negate) i += 1;
|
|
@@ -681,8 +1174,8 @@ function parseRegex(src) {
|
|
|
681
1174
|
}
|
|
682
1175
|
if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
|
|
683
1176
|
i += 1;
|
|
684
|
-
const base = (
|
|
685
|
-
return { k: "class", test: negate ? (
|
|
1177
|
+
const base = (c2) => tests.some((t) => t(c2));
|
|
1178
|
+
return { k: "class", test: negate ? (c2) => !base(c2) : base };
|
|
686
1179
|
}
|
|
687
1180
|
function parseClassMember() {
|
|
688
1181
|
let lo;
|
|
@@ -711,12 +1204,12 @@ function parseRegex(src) {
|
|
|
711
1204
|
}
|
|
712
1205
|
const a = lo.codePointAt(0);
|
|
713
1206
|
const b = hi.codePointAt(0);
|
|
714
|
-
return (
|
|
715
|
-
const p =
|
|
1207
|
+
return (c2) => {
|
|
1208
|
+
const p = c2.codePointAt(0);
|
|
716
1209
|
return p >= a && p <= b;
|
|
717
1210
|
};
|
|
718
1211
|
}
|
|
719
|
-
return (
|
|
1212
|
+
return (c2) => c2 === lo;
|
|
720
1213
|
}
|
|
721
1214
|
const ast = parseAlt();
|
|
722
1215
|
if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
|
|
@@ -784,6 +1277,9 @@ function isBroadCommandPattern(pattern) {
|
|
|
784
1277
|
}
|
|
785
1278
|
return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
|
|
786
1279
|
}
|
|
1280
|
+
function codePointLength(text) {
|
|
1281
|
+
return [...text].length;
|
|
1282
|
+
}
|
|
787
1283
|
|
|
788
1284
|
// src/lessons/ranking-text.ts
|
|
789
1285
|
var K1 = 1.5;
|
|
@@ -810,7 +1306,7 @@ var STOP = /* @__PURE__ */ new Set([
|
|
|
810
1306
|
"its",
|
|
811
1307
|
"must"
|
|
812
1308
|
]);
|
|
813
|
-
function
|
|
1309
|
+
function tokenize2(text) {
|
|
814
1310
|
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
815
1311
|
}
|
|
816
1312
|
function queryTerms(query) {
|
|
@@ -818,7 +1314,7 @@ function queryTerms(query) {
|
|
|
818
1314
|
if (query.keyword !== void 0) parts.push(query.keyword);
|
|
819
1315
|
if (query.file !== void 0) parts.push(query.file);
|
|
820
1316
|
if (query.command !== void 0) parts.push(query.command);
|
|
821
|
-
return
|
|
1317
|
+
return tokenize2(parts.join(" "));
|
|
822
1318
|
}
|
|
823
1319
|
function buildCorpus(graph) {
|
|
824
1320
|
const docs = [];
|
|
@@ -827,7 +1323,7 @@ function buildCorpus(graph) {
|
|
|
827
1323
|
let n = 0;
|
|
828
1324
|
for (const lesson of Object.values(graph.lessons)) {
|
|
829
1325
|
if (lesson.status !== "active") continue;
|
|
830
|
-
const toks =
|
|
1326
|
+
const toks = tokenize2(lesson.rule);
|
|
831
1327
|
n += 1;
|
|
832
1328
|
total += toks.length;
|
|
833
1329
|
docs.push(toks.length);
|
|
@@ -839,7 +1335,7 @@ function buildCorpus(graph) {
|
|
|
839
1335
|
return { idf, avgdl: total / N || 1 };
|
|
840
1336
|
}
|
|
841
1337
|
function bm25(terms, ruleText, corpus) {
|
|
842
|
-
const toks =
|
|
1338
|
+
const toks = tokenize2(ruleText);
|
|
843
1339
|
const dl = toks.length || 1;
|
|
844
1340
|
const tf = /* @__PURE__ */ new Map();
|
|
845
1341
|
for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
@@ -856,7 +1352,7 @@ function bm25(terms, ruleText, corpus) {
|
|
|
856
1352
|
// src/lessons/keyword-signal.ts
|
|
857
1353
|
var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
|
|
858
1354
|
function isLowSignalKeyword(pattern) {
|
|
859
|
-
return
|
|
1355
|
+
return tokenize2(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
|
|
860
1356
|
}
|
|
861
1357
|
function splitRawTokens(pattern) {
|
|
862
1358
|
return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
@@ -864,23 +1360,23 @@ function splitRawTokens(pattern) {
|
|
|
864
1360
|
function keywordNeedleLosesTokens(pattern) {
|
|
865
1361
|
const raw = splitRawTokens(pattern);
|
|
866
1362
|
if (raw.length < 2) return false;
|
|
867
|
-
return
|
|
1363
|
+
return tokenize2(pattern).length !== raw.length;
|
|
868
1364
|
}
|
|
869
1365
|
|
|
870
1366
|
// src/lessons/trigger-effectiveness.ts
|
|
871
1367
|
function ineffectiveTriggers(graph, triggerIds) {
|
|
872
|
-
const
|
|
1368
|
+
const out2 = [];
|
|
873
1369
|
for (const id of triggerIds) {
|
|
874
1370
|
const trigger = graph.triggers[id];
|
|
875
1371
|
if (trigger === void 0) continue;
|
|
876
1372
|
const reason = ineffectiveReason(trigger.kind, trigger.pattern);
|
|
877
|
-
if (reason !== null)
|
|
1373
|
+
if (reason !== null) out2.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
|
|
878
1374
|
}
|
|
879
|
-
return
|
|
1375
|
+
return out2;
|
|
880
1376
|
}
|
|
881
1377
|
function ineffectiveReason(kind, pattern) {
|
|
882
1378
|
if (kind === "keyword") {
|
|
883
|
-
if (
|
|
1379
|
+
if (tokenize2(pattern).length === 0) {
|
|
884
1380
|
return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
|
|
885
1381
|
}
|
|
886
1382
|
if (keywordNeedleLosesTokens(pattern)) {
|
|
@@ -913,9 +1409,19 @@ function blockingDeadTriggers(graph, triggerIds) {
|
|
|
913
1409
|
function assertRuleShape(rule) {
|
|
914
1410
|
const trimmed = rule.trim();
|
|
915
1411
|
if (trimmed.length === 0) throw new EmptyRuleError();
|
|
916
|
-
|
|
1412
|
+
const length = codePointLength(trimmed);
|
|
1413
|
+
if (length > MAX_RULE_LENGTH) throw new RuleTooLongError(length, MAX_RULE_LENGTH);
|
|
917
1414
|
return trimmed;
|
|
918
1415
|
}
|
|
1416
|
+
function ensureTopic(graph, topic, options) {
|
|
1417
|
+
if (!/^[a-z0-9-]+$/.test(topic)) throw new InvalidTopicIdError(topic);
|
|
1418
|
+
if (graph.topics[topic] !== void 0) return false;
|
|
1419
|
+
if (options.allowNewTopic !== true) throw new UnknownTopicError(topic);
|
|
1420
|
+
const summary = options.topicSummary?.trim() ?? "";
|
|
1421
|
+
if (summary.length === 0) throw new TopicSummaryRequiredError(topic);
|
|
1422
|
+
graph.topics[topic] = { summary };
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
919
1425
|
function skipsTriggerGates(input, options) {
|
|
920
1426
|
return options.allowNoTrigger === true || input.scope === "always";
|
|
921
1427
|
}
|
|
@@ -931,16 +1437,40 @@ function assertTriggerInputs(input, options, existingTriggerCount) {
|
|
|
931
1437
|
if (broad !== void 0) throw new BroadCommandPatternError(broad);
|
|
932
1438
|
}
|
|
933
1439
|
}
|
|
934
|
-
function
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1440
|
+
function dropDeadCommandTriggers(graph, merged, options) {
|
|
1441
|
+
if (options.allowNoTrigger === true) return { ...merged, dropped: [] };
|
|
1442
|
+
const dropped = ineffectiveTriggers(graph, merged.triggerIds).filter(
|
|
1443
|
+
(t) => t.kind === "command_pattern"
|
|
1444
|
+
);
|
|
1445
|
+
const dead = new Set(dropped.map((t) => t.id));
|
|
1446
|
+
for (const id of merged.newTriggerIds) if (dead.has(id)) delete graph.triggers[id];
|
|
1447
|
+
return {
|
|
1448
|
+
triggerIds: merged.triggerIds.filter((id) => !dead.has(id)),
|
|
1449
|
+
newTriggerIds: merged.newTriggerIds.filter((id) => !dead.has(id)),
|
|
1450
|
+
dropped
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function deadCommandWarning(trigger) {
|
|
1454
|
+
return {
|
|
1455
|
+
code: "DEAD_COMMAND_PATTERN",
|
|
1456
|
+
message: `Dropped command trigger ${JSON.stringify(trigger.pattern)} (not saved): ${trigger.reason}.`
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function assertRecallable(graph, resultingTriggers, dropped) {
|
|
1460
|
+
const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
|
|
1461
|
+
const dead = [...dropped, ...blockingDead];
|
|
1462
|
+
if (dead.length > 0 && blockingDead.length === resultingTriggers.length) {
|
|
1463
|
+
throw new UnrecallableLessonError(dead);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
941
1467
|
// src/lessons/glob-breadth.ts
|
|
942
1468
|
var WILDCARD = /[*?[\]]/;
|
|
1469
|
+
function isNegatedGlob(pattern) {
|
|
1470
|
+
return pattern.startsWith("!");
|
|
1471
|
+
}
|
|
943
1472
|
function globNarrowness(pattern) {
|
|
1473
|
+
if (isNegatedGlob(pattern)) return 0;
|
|
944
1474
|
const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
945
1475
|
if (segments.length === 0) return 0;
|
|
946
1476
|
let literal = 0;
|
|
@@ -955,6 +1485,258 @@ var BROAD_GLOB_NARROWNESS = 0.34;
|
|
|
955
1485
|
function isBroadFileGlob(pattern) {
|
|
956
1486
|
return globNarrowness(pattern) < BROAD_GLOB_NARROWNESS;
|
|
957
1487
|
}
|
|
1488
|
+
|
|
1489
|
+
// src/lessons/glob-dp.ts
|
|
1490
|
+
var isDotSegment = (s) => s === "." || s === "..";
|
|
1491
|
+
function matchSegments(alt, segments, work) {
|
|
1492
|
+
const n = segments.length;
|
|
1493
|
+
let next = new Uint8Array(n + 1);
|
|
1494
|
+
next[n] = 1;
|
|
1495
|
+
for (let i = alt.length - 1; i >= 0; i -= 1) {
|
|
1496
|
+
const seg = alt[i];
|
|
1497
|
+
const cur = new Uint8Array(n + 1);
|
|
1498
|
+
for (let j = n; j >= 0; j -= 1) {
|
|
1499
|
+
if (--work.remaining <= 0) return false;
|
|
1500
|
+
if (seg.k === "globstar") {
|
|
1501
|
+
const eat = j < n && !isDotSegment(segments[j]) && (cur[j + 1] === 1 || next[j + 1] === 1);
|
|
1502
|
+
cur[j] = !seg.min1 && next[j] === 1 || eat ? 1 : 0;
|
|
1503
|
+
} else if (j < n && next[j + 1] === 1) {
|
|
1504
|
+
const text = segments[j];
|
|
1505
|
+
const ok = seg.literal !== null ? text === seg.literal : matchSegment(seg, text, j < n - 1, work);
|
|
1506
|
+
cur[j] = ok ? 1 : 0;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (seg.k === "globstar" && i >= 1 && tailMatchesNothing(alt, i + 1)) cur[n] = 1;
|
|
1510
|
+
next = cur;
|
|
1511
|
+
}
|
|
1512
|
+
return next[0] === 1;
|
|
1513
|
+
}
|
|
1514
|
+
function tailMatchesNothing(alt, from) {
|
|
1515
|
+
const empty = alt[from];
|
|
1516
|
+
const rest = alt[from + 1];
|
|
1517
|
+
if (empty?.k !== "segment" || !empty.matchesEmpty) return false;
|
|
1518
|
+
return rest === void 0 || rest.k === "globstar" && !rest.min1 && from + 2 === alt.length;
|
|
1519
|
+
}
|
|
1520
|
+
function matchSegment(seg, text, followedBySlash, work) {
|
|
1521
|
+
if (seg.guarded && (isDotSegment(text) || text === "" && !followedBySlash)) return false;
|
|
1522
|
+
const tokens = seg.tokens;
|
|
1523
|
+
const len = text.length;
|
|
1524
|
+
let next = new Uint8Array(len + 1);
|
|
1525
|
+
next[len] = 1;
|
|
1526
|
+
for (let k = tokens.length - 1; k >= 0; k -= 1) {
|
|
1527
|
+
const tok = tokens[k];
|
|
1528
|
+
const cur = new Uint8Array(len + 1);
|
|
1529
|
+
work.remaining -= len + 1;
|
|
1530
|
+
if (work.remaining <= 0) return false;
|
|
1531
|
+
for (let c2 = len; c2 >= 0; c2 -= 1) {
|
|
1532
|
+
cur[c2] = tokenMatches(tok, text, c2, next, cur) ? 1 : 0;
|
|
1533
|
+
}
|
|
1534
|
+
next = cur;
|
|
1535
|
+
}
|
|
1536
|
+
return next[0] === 1;
|
|
1537
|
+
}
|
|
1538
|
+
function tokenMatches(tok, text, c2, next, cur) {
|
|
1539
|
+
const ch = text[c2];
|
|
1540
|
+
switch (tok.k) {
|
|
1541
|
+
case "star":
|
|
1542
|
+
return next[c2] === 1 || ch !== void 0 && cur[c2 + 1] === 1;
|
|
1543
|
+
case "one":
|
|
1544
|
+
return ch !== void 0 && next[c2 + 1] === 1;
|
|
1545
|
+
case "lit":
|
|
1546
|
+
return ch === tok.ch && next[c2 + 1] === 1;
|
|
1547
|
+
case "class":
|
|
1548
|
+
return ch !== void 0 && tok.test(ch) && next[c2 + 1] === 1 || tok.literal !== null && text.startsWith(tok.literal, c2) && next[c2 + tok.literal.length] === 1;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/lessons/glob-safety.ts
|
|
1553
|
+
var MAX_GLOB_PATH_LENGTH = 4096;
|
|
1554
|
+
var MATCH_WORK_LIMIT = 2e5;
|
|
1555
|
+
var CACHE_LIMIT = 2e3;
|
|
1556
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
1557
|
+
function unsafeGlobFinding(triggerId, trigger) {
|
|
1558
|
+
if (trigger.kind !== "file_glob" || trigger.pattern.includes("\\")) return null;
|
|
1559
|
+
const reason = parseGlob(trigger.pattern);
|
|
1560
|
+
if (typeof reason !== "string") return null;
|
|
1561
|
+
return {
|
|
1562
|
+
level: "error",
|
|
1563
|
+
code: "UNSAFE_GLOB_PATTERN",
|
|
1564
|
+
message: `Trigger "${triggerId}" has a file_glob outside the safe glob subset (${trigger.pattern.slice(0, 120)}): ${reason}. Recall treats it as a non-match. Use only *, **, ?, [...] and {a,b}.`,
|
|
1565
|
+
triggerId
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function getGlobMatcher(pattern) {
|
|
1569
|
+
const hit = cache2.get(pattern);
|
|
1570
|
+
if (hit !== void 0) return hit;
|
|
1571
|
+
if (cache2.size >= CACHE_LIMIT) cache2.clear();
|
|
1572
|
+
const composed = pattern.normalize("NFC");
|
|
1573
|
+
const parsed = parseGlob(composed);
|
|
1574
|
+
const matcher = typeof parsed === "string" ? null : build(composed, parsed);
|
|
1575
|
+
cache2.set(pattern, matcher);
|
|
1576
|
+
return matcher;
|
|
1577
|
+
}
|
|
1578
|
+
function build(pattern, parsed) {
|
|
1579
|
+
const alts = parsed.alternatives.map(prepare);
|
|
1580
|
+
return {
|
|
1581
|
+
test(rawPath, budget) {
|
|
1582
|
+
const path = rawPath.normalize("NFC");
|
|
1583
|
+
if (path === pattern) return true;
|
|
1584
|
+
if (path === "" || path.length > MAX_GLOB_PATH_LENGTH) return false;
|
|
1585
|
+
const live = alts.filter((a) => mayMatch(a, path));
|
|
1586
|
+
if (live.length === 0) return parsed.negated;
|
|
1587
|
+
const limit = Math.min(MATCH_WORK_LIMIT, budget?.remaining ?? MATCH_WORK_LIMIT);
|
|
1588
|
+
const work = { remaining: limit };
|
|
1589
|
+
const segments = path.split("/");
|
|
1590
|
+
const hit = live.some((a) => matchSegments(a.alt, segments, work));
|
|
1591
|
+
if (budget !== void 0) budget.remaining -= limit - work.remaining;
|
|
1592
|
+
return work.remaining > 0 && hit !== parsed.negated;
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
function mayMatch({ head, tail }, path) {
|
|
1597
|
+
if (!path.endsWith(tail)) return false;
|
|
1598
|
+
if (head === null) return true;
|
|
1599
|
+
return path.startsWith(head) && (path.length === head.length || path[head.length] === "/");
|
|
1600
|
+
}
|
|
1601
|
+
function prepare(alt) {
|
|
1602
|
+
const first = alt[0];
|
|
1603
|
+
const last = alt[alt.length - 1];
|
|
1604
|
+
let tail = "";
|
|
1605
|
+
if (last?.k === "segment" && !last.matchesEmpty) {
|
|
1606
|
+
for (let k = last.tokens.length - 1; k >= 0; k -= 1) {
|
|
1607
|
+
const tok = last.tokens[k];
|
|
1608
|
+
if (tok.k !== "lit") break;
|
|
1609
|
+
tail = tok.ch + tail;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return { alt, head: first?.k === "segment" ? first.literal : null, tail };
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/lessons/file-glob-liveness.ts
|
|
1616
|
+
function missingGlobState(pattern, history) {
|
|
1617
|
+
const matcher = getGlobMatcher(pattern);
|
|
1618
|
+
if (history === null || matcher === null) return "pending";
|
|
1619
|
+
const matchesAny = (paths) => [...paths].some((p) => matcher.test(p));
|
|
1620
|
+
if (matchesAny(history.tracked)) return "live";
|
|
1621
|
+
if (matchesAny(history.renamedAway)) return "dead";
|
|
1622
|
+
if (!picomatch.scan(pattern).isGlob && matchesAny(history.deleted)) return "dead";
|
|
1623
|
+
return "pending";
|
|
1624
|
+
}
|
|
1625
|
+
var MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
1626
|
+
function runGit(cwd, args, timeoutMs) {
|
|
1627
|
+
const r = spawnSync("git", [...args], {
|
|
1628
|
+
cwd,
|
|
1629
|
+
encoding: "utf8",
|
|
1630
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
1631
|
+
timeout: timeoutMs,
|
|
1632
|
+
windowsHide: true
|
|
1633
|
+
});
|
|
1634
|
+
const status = r.error === void 0 ? r.status ?? -1 : -1;
|
|
1635
|
+
return { status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/lessons/git-path-history.ts
|
|
1639
|
+
var GIT_SCAN_TIMEOUT_MS = 3e3;
|
|
1640
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
1641
|
+
function readGitPathHistory(projectRoot) {
|
|
1642
|
+
if (!cache3.has(projectRoot)) cache3.set(projectRoot, scanGitPathHistory(projectRoot));
|
|
1643
|
+
return cache3.get(projectRoot) ?? null;
|
|
1644
|
+
}
|
|
1645
|
+
var LOG_ARGS = [
|
|
1646
|
+
"log",
|
|
1647
|
+
"HEAD",
|
|
1648
|
+
"--relative",
|
|
1649
|
+
"-M",
|
|
1650
|
+
"--diff-filter=DR",
|
|
1651
|
+
"--name-status",
|
|
1652
|
+
"-z",
|
|
1653
|
+
"--no-color",
|
|
1654
|
+
"--no-show-signature",
|
|
1655
|
+
"--pretty=format:"
|
|
1656
|
+
];
|
|
1657
|
+
function scanGitPathHistory(projectRoot, timeoutMs = GIT_SCAN_TIMEOUT_MS) {
|
|
1658
|
+
const tracked = runGit(projectRoot, ["ls-files", "-z"], timeoutMs);
|
|
1659
|
+
if (tracked.status !== 0) return null;
|
|
1660
|
+
const log = runGit(projectRoot, LOG_ARGS, timeoutMs);
|
|
1661
|
+
if (log.status !== 0) return null;
|
|
1662
|
+
return {
|
|
1663
|
+
tracked: new Set(tracked.stdout.split("\0").filter(Boolean)),
|
|
1664
|
+
...parseRemovals(log.stdout)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function parseRemovals(out2) {
|
|
1668
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
1669
|
+
const renamedAway = /* @__PURE__ */ new Set();
|
|
1670
|
+
const tokens = out2.split("\0");
|
|
1671
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1672
|
+
const status = tokens[i] ?? "";
|
|
1673
|
+
const path = tokens[i + 1] ?? "";
|
|
1674
|
+
if (status.startsWith("D")) {
|
|
1675
|
+
if (path !== "") deleted.add(path);
|
|
1676
|
+
i += 1;
|
|
1677
|
+
} else if (status.startsWith("R")) {
|
|
1678
|
+
if (path !== "") renamedAway.add(path);
|
|
1679
|
+
i += 2;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return { deleted, renamedAway };
|
|
1683
|
+
}
|
|
1684
|
+
var BASE_REL = ".agentsmesh/lessons";
|
|
1685
|
+
function lessonsPaths(projectRoot) {
|
|
1686
|
+
const base = join(projectRoot, BASE_REL);
|
|
1687
|
+
return {
|
|
1688
|
+
base,
|
|
1689
|
+
graph: join(base, "lessons.json"),
|
|
1690
|
+
config: join(base, "config.json"),
|
|
1691
|
+
journal: join(base, "journal.md"),
|
|
1692
|
+
index: join(base, "index.yaml"),
|
|
1693
|
+
topicsDir: join(base, "topics")
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function toRelPath(projectRoot, absolute) {
|
|
1697
|
+
return relative(projectRoot, absolute).split(sep).join("/");
|
|
1698
|
+
}
|
|
1699
|
+
var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
1700
|
+
|
|
1701
|
+
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
1702
|
+
|
|
1703
|
+
**Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command> --session auto\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always --session auto\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
|
|
1704
|
+
|
|
1705
|
+
**Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
|
|
1706
|
+
|
|
1707
|
+
**Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
|
|
1708
|
+
|
|
1709
|
+
// src/lessons/project-files.ts
|
|
1710
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
1711
|
+
var MAX_FILES = 2e5;
|
|
1712
|
+
function projectFilesOf(paths, gitHistory) {
|
|
1713
|
+
return Object.assign(new Set(paths), { gitHistory });
|
|
1714
|
+
}
|
|
1715
|
+
function gitHistoryOf(paths) {
|
|
1716
|
+
return paths.gitHistory?.() ?? null;
|
|
1717
|
+
}
|
|
1718
|
+
function listProjectFiles(projectRoot, maxFiles = MAX_FILES) {
|
|
1719
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
1720
|
+
try {
|
|
1721
|
+
const stack = [projectRoot];
|
|
1722
|
+
while (stack.length > 0) {
|
|
1723
|
+
const dir = stack.pop();
|
|
1724
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1725
|
+
if (entry.isDirectory()) {
|
|
1726
|
+
if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
1727
|
+
} else if (entry.isFile()) {
|
|
1728
|
+
out2.add(toRelPath(projectRoot, join(dir, entry.name)));
|
|
1729
|
+
if (out2.size > maxFiles) return null;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
} catch {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
return projectFilesOf(out2, () => readGitPathHistory(projectRoot));
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/lessons/validate-liveness.ts
|
|
958
1740
|
function activeTriggerIds(graph) {
|
|
959
1741
|
const ids = /* @__PURE__ */ new Set();
|
|
960
1742
|
for (const lesson of Object.values(graph.lessons)) {
|
|
@@ -963,32 +1745,42 @@ function activeTriggerIds(graph) {
|
|
|
963
1745
|
}
|
|
964
1746
|
return ids;
|
|
965
1747
|
}
|
|
966
|
-
function
|
|
967
|
-
const active = activeTriggerIds(graph);
|
|
1748
|
+
function fileGlobLiveness(graph, knownPaths, triggerIds) {
|
|
1749
|
+
const active = triggerIds === void 0 ? activeTriggerIds(graph) : new Set(triggerIds);
|
|
968
1750
|
const paths = [...knownPaths];
|
|
969
|
-
const
|
|
1751
|
+
const missing = [];
|
|
970
1752
|
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
971
|
-
if (trigger.kind !== "file_glob") continue;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (!paths.some((p) =>
|
|
1753
|
+
if (trigger.kind !== "file_glob" || !active.has(triggerId)) continue;
|
|
1754
|
+
const matcher = getGlobMatcher(trigger.pattern);
|
|
1755
|
+
if (matcher === null) continue;
|
|
1756
|
+
if (!paths.some((p) => matcher.test(p))) missing.push([triggerId, trigger.pattern]);
|
|
975
1757
|
}
|
|
976
|
-
|
|
1758
|
+
const dead = /* @__PURE__ */ new Set();
|
|
1759
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1760
|
+
if (missing.length === 0) return { dead, pending };
|
|
1761
|
+
const history = gitHistoryOf(knownPaths);
|
|
1762
|
+
for (const [triggerId, pattern] of missing) {
|
|
1763
|
+
const state = missingGlobState(pattern, history);
|
|
1764
|
+
if (state === "dead") dead.add(triggerId);
|
|
1765
|
+
else if (state === "pending") pending.add(triggerId);
|
|
1766
|
+
}
|
|
1767
|
+
return { dead, pending };
|
|
977
1768
|
}
|
|
978
1769
|
function collectDeadFileGlobs(graph, findings, knownPaths) {
|
|
979
|
-
for (const triggerId of
|
|
1770
|
+
for (const triggerId of fileGlobLiveness(graph, knownPaths).dead) {
|
|
980
1771
|
findings.push({
|
|
981
1772
|
level: "warning",
|
|
982
1773
|
code: "DEAD_FILE_GLOB",
|
|
983
|
-
message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file
|
|
1774
|
+
message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file, and git history shows its path was renamed or deleted \u2014 the lesson is unreachable via this trigger. Re-point it at the current path, or detach it with \`lessons untrigger\`, or run \`lessons prune --apply\`.`,
|
|
984
1775
|
triggerId
|
|
985
1776
|
});
|
|
986
1777
|
}
|
|
987
1778
|
}
|
|
988
1779
|
function fileGlobMatchCount(pattern, knownPaths) {
|
|
989
|
-
const
|
|
1780
|
+
const matcher = getGlobMatcher(pattern);
|
|
1781
|
+
if (matcher === null) return 0;
|
|
990
1782
|
let n = 0;
|
|
991
|
-
for (const p of knownPaths) if (
|
|
1783
|
+
for (const p of knownPaths) if (matcher.test(p)) n += 1;
|
|
992
1784
|
return n;
|
|
993
1785
|
}
|
|
994
1786
|
var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
|
|
@@ -1040,10 +1832,10 @@ var WIDE_GLOB_MATCH_COUNT = 40;
|
|
|
1040
1832
|
var MAX_RECOMMENDED_TRIGGERS = 8;
|
|
1041
1833
|
function isBroadGlob(pattern) {
|
|
1042
1834
|
const p = pattern.trim();
|
|
1043
|
-
if (p === "*" || p === "**") return true;
|
|
1835
|
+
if (p === "*" || p === "**" || isNegatedGlob(p)) return true;
|
|
1044
1836
|
if (!p.includes("**")) return false;
|
|
1045
|
-
const
|
|
1046
|
-
return
|
|
1837
|
+
const basename6 = p.slice(p.lastIndexOf("/") + 1);
|
|
1838
|
+
return basename6.startsWith("*");
|
|
1047
1839
|
}
|
|
1048
1840
|
function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
1049
1841
|
const lesson = graph.lessons[lessonId];
|
|
@@ -1084,12 +1876,19 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
|
1084
1876
|
});
|
|
1085
1877
|
}
|
|
1086
1878
|
if (knownPaths !== void 0) {
|
|
1087
|
-
const dead =
|
|
1088
|
-
const deadHere = lesson.triggers
|
|
1879
|
+
const { dead, pending } = fileGlobLiveness(graph, knownPaths, lesson.triggers);
|
|
1880
|
+
const deadHere = patternsIn(graph, lesson.triggers, dead);
|
|
1089
1881
|
if (deadHere.length > 0) {
|
|
1090
1882
|
warnings.push({
|
|
1091
1883
|
code: "DEAD_GLOB",
|
|
1092
|
-
message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file
|
|
1884
|
+
message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file, and git history shows the path was renamed or deleted \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const pendingHere = patternsIn(graph, lesson.triggers, pending);
|
|
1888
|
+
if (pendingHere.length > 0) {
|
|
1889
|
+
warnings.push({
|
|
1890
|
+
code: "PENDING_GLOB",
|
|
1891
|
+
message: `Lesson "${lessonId}" has file_glob trigger(s) (${pendingHere.join(", ")}) whose path does not exist yet \u2014 the trigger will fire once it does, so it is kept. If the path is a typo, re-point it.`
|
|
1093
1892
|
});
|
|
1094
1893
|
}
|
|
1095
1894
|
const wide = triggers.filter((t) => t.kind === "file_glob" && !isBroadGlob(t.pattern)).filter((t) => fileGlobMatchCount(t.pattern, knownPaths) > WIDE_GLOB_MATCH_COUNT).map((t) => t.pattern);
|
|
@@ -1102,18 +1901,21 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
|
1102
1901
|
}
|
|
1103
1902
|
return warnings;
|
|
1104
1903
|
}
|
|
1904
|
+
function patternsIn(graph, ids, keep) {
|
|
1905
|
+
return ids.filter((id) => keep.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
|
|
1906
|
+
}
|
|
1105
1907
|
|
|
1106
1908
|
// src/lessons/capture-near-duplicate.ts
|
|
1107
1909
|
var NEAR_DUPLICATE_THRESHOLD = 0.6;
|
|
1108
1910
|
function nearDuplicateWarning(graph, lessonId) {
|
|
1109
1911
|
const subject = graph.lessons[lessonId];
|
|
1110
1912
|
if (subject === void 0) return null;
|
|
1111
|
-
const subjectTokens = new Set(
|
|
1913
|
+
const subjectTokens = new Set(tokenize2(subject.rule));
|
|
1112
1914
|
if (subjectTokens.size === 0) return null;
|
|
1113
1915
|
let best = null;
|
|
1114
1916
|
for (const [id, other] of Object.entries(graph.lessons)) {
|
|
1115
1917
|
if (id === lessonId || other.status !== "active") continue;
|
|
1116
|
-
const otherTokens = new Set(
|
|
1918
|
+
const otherTokens = new Set(tokenize2(other.rule));
|
|
1117
1919
|
if (otherTokens.size === 0) continue;
|
|
1118
1920
|
const score = jaccard(subjectTokens, otherTokens);
|
|
1119
1921
|
if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
|
|
@@ -1131,6 +1933,26 @@ function jaccard(a, b) {
|
|
|
1131
1933
|
for (const t of a) if (b.has(t)) intersection += 1;
|
|
1132
1934
|
return intersection / (a.size + b.size - intersection);
|
|
1133
1935
|
}
|
|
1936
|
+
var LEFTOVER = /\.(?:\d+\.tmp|[\w-]+\.stale)$/;
|
|
1937
|
+
var LEFTOVER_AGE_MS = 6e4;
|
|
1938
|
+
function sweepLessonsLeftovers(projectRoot, now = Date.now()) {
|
|
1939
|
+
const dir = lessonsPaths(projectRoot).base;
|
|
1940
|
+
let names;
|
|
1941
|
+
try {
|
|
1942
|
+
names = readdirSync(dir);
|
|
1943
|
+
} catch {
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
for (const name of names) {
|
|
1947
|
+
if (!LEFTOVER.test(name)) continue;
|
|
1948
|
+
const path = join(dir, name);
|
|
1949
|
+
try {
|
|
1950
|
+
if (now - statSync(path).mtimeMs > LEFTOVER_AGE_MS)
|
|
1951
|
+
rmSync(path, { recursive: true, force: true });
|
|
1952
|
+
} catch {
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1134
1956
|
|
|
1135
1957
|
// src/core/errors.ts
|
|
1136
1958
|
var AgentsMeshError = class extends Error {
|
|
@@ -1169,62 +1991,375 @@ var FileSystemError = class extends AgentsMeshError {
|
|
|
1169
1991
|
this.errnoCode = options?.errnoCode;
|
|
1170
1992
|
}
|
|
1171
1993
|
};
|
|
1994
|
+
var execFileAsync = promisify(execFile);
|
|
1995
|
+
var PS_TIMEOUT_MS = 2e3;
|
|
1996
|
+
var self;
|
|
1997
|
+
async function processIdentity(pid, platform = process.platform) {
|
|
1998
|
+
if (!Number.isInteger(pid) || pid <= 0 || platform === "win32") return null;
|
|
1999
|
+
try {
|
|
2000
|
+
return platform === "linux" ? await linuxIdentity(pid) : await psIdentity(pid);
|
|
2001
|
+
} catch {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function selfIdentity() {
|
|
2006
|
+
self ??= processIdentity(process.pid);
|
|
2007
|
+
return self;
|
|
2008
|
+
}
|
|
2009
|
+
function linuxStartIdentity(stat3, bootId) {
|
|
2010
|
+
const start = stat3.slice(stat3.lastIndexOf(")") + 2).split(" ")[19];
|
|
2011
|
+
const boot = bootId.trim();
|
|
2012
|
+
if (start === void 0 || !/^\d+$/.test(start) || boot === "") return null;
|
|
2013
|
+
return `${boot}:${start}`;
|
|
2014
|
+
}
|
|
2015
|
+
async function linuxIdentity(pid) {
|
|
2016
|
+
const [stat3, bootId] = await Promise.all([
|
|
2017
|
+
readFile(`/proc/${pid}/stat`, "utf-8"),
|
|
2018
|
+
readFile("/proc/sys/kernel/random/boot_id", "utf-8")
|
|
2019
|
+
]);
|
|
2020
|
+
return linuxStartIdentity(stat3, bootId);
|
|
2021
|
+
}
|
|
2022
|
+
async function psIdentity(pid) {
|
|
2023
|
+
const { stdout } = await execFileAsync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
2024
|
+
env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
|
|
2025
|
+
timeout: PS_TIMEOUT_MS
|
|
2026
|
+
});
|
|
2027
|
+
const start = stdout.trim();
|
|
2028
|
+
return start === "" ? null : start;
|
|
2029
|
+
}
|
|
2030
|
+
var TRANSIENT_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
2031
|
+
var ATTEMPTS = 5;
|
|
2032
|
+
var BASE_DELAY_MS = 25;
|
|
2033
|
+
var pause = new Int32Array(new SharedArrayBuffer(4));
|
|
2034
|
+
function isTransientFsError(err) {
|
|
2035
|
+
const code = err?.code;
|
|
2036
|
+
return typeof code === "string" && TRANSIENT_CODES.has(code);
|
|
2037
|
+
}
|
|
2038
|
+
async function retryTransient(op) {
|
|
2039
|
+
for (let attempt = 1; ; attempt++) {
|
|
2040
|
+
try {
|
|
2041
|
+
return await op();
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
if (!isTransientFsError(err) || attempt >= ATTEMPTS) throw err;
|
|
2044
|
+
await setTimeout(BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
function retryTransientSync(op) {
|
|
2049
|
+
for (let attempt = 1; ; attempt++) {
|
|
2050
|
+
try {
|
|
2051
|
+
return op();
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
if (!isTransientFsError(err) || attempt >= ATTEMPTS) throw err;
|
|
2054
|
+
Atomics.wait(pause, 0, 0, BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY", "EEXIST"]);
|
|
2059
|
+
async function renameWithRetry(from, to, options = {}) {
|
|
2060
|
+
const attempts = options.attempts ?? 5;
|
|
2061
|
+
const delayMs = options.delayMs ?? 50;
|
|
2062
|
+
for (let attempt = 0; ; attempt++) {
|
|
2063
|
+
try {
|
|
2064
|
+
await rename(from, to);
|
|
2065
|
+
return;
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
const code = err.code;
|
|
2068
|
+
const transient = code !== void 0 && TRANSIENT_RENAME_CODES.has(code);
|
|
2069
|
+
if (!transient || attempt >= attempts - 1) throw err;
|
|
2070
|
+
await setTimeout(delayMs * 2 ** attempt);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
var HOLDER_FILE = "holder.json";
|
|
2075
|
+
var OWNER_PREFIX = "owner-";
|
|
2076
|
+
var YOUNG_LOCK_GRACE_MS = 2e3;
|
|
2077
|
+
var PID_REUSE_PROBE_AFTER_MS = 2e3;
|
|
2078
|
+
function ownerPath(lockPath, token) {
|
|
2079
|
+
return join(lockPath, `${OWNER_PREFIX}${token}`);
|
|
2080
|
+
}
|
|
2081
|
+
function holderPath(lockPath) {
|
|
2082
|
+
return join(lockPath, HOLDER_FILE);
|
|
2083
|
+
}
|
|
2084
|
+
function errorCode(err) {
|
|
2085
|
+
return err?.code;
|
|
2086
|
+
}
|
|
2087
|
+
var LockPathNotFolderError = class extends Error {
|
|
2088
|
+
constructor(lockPath) {
|
|
2089
|
+
super(
|
|
2090
|
+
`${lockPath.replaceAll("\\", "/")} is a file, but agentsmesh keeps its lock there as a folder. Delete it and run the command again.`
|
|
2091
|
+
);
|
|
2092
|
+
this.name = "LockPathNotFolderError";
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
async function ownerTokens(dir) {
|
|
2096
|
+
try {
|
|
2097
|
+
const entries = await readdir(dir);
|
|
2098
|
+
return entries.filter((e) => e.startsWith(OWNER_PREFIX)).map((e) => e.slice(OWNER_PREFIX.length));
|
|
2099
|
+
} catch (err) {
|
|
2100
|
+
if (errorCode(err) === "ENOENT") return null;
|
|
2101
|
+
if (errorCode(err) === "ENOTDIR") throw new LockPathNotFolderError(dir);
|
|
2102
|
+
throw err;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async function readHolderRaw(dir) {
|
|
2106
|
+
return readFile(holderPath(dir), "utf-8").catch(() => null);
|
|
2107
|
+
}
|
|
2108
|
+
function holderToken(raw) {
|
|
2109
|
+
return raw === null ? void 0 : parseMetadata(raw)?.token;
|
|
2110
|
+
}
|
|
2111
|
+
async function inspectLock(lockPath) {
|
|
2112
|
+
const tokens = await ownerTokens(lockPath);
|
|
2113
|
+
if (tokens === null) return { kind: "gone" };
|
|
2114
|
+
const raw = await readHolderRaw(lockPath);
|
|
2115
|
+
const meta = raw === null ? null : parseMetadata(raw);
|
|
2116
|
+
const [only] = tokens;
|
|
2117
|
+
if (meta && tokens.length === 1 && only !== void 0 && meta.token === only) {
|
|
2118
|
+
return { kind: "held", token: only, meta };
|
|
2119
|
+
}
|
|
2120
|
+
if (meta && raw !== null && tokens.length === 0 && meta.token === void 0) {
|
|
2121
|
+
return { kind: "legacy", meta, raw };
|
|
2122
|
+
}
|
|
2123
|
+
const age = await dirAgeMs(lockPath);
|
|
2124
|
+
if (age === null) return { kind: "gone" };
|
|
2125
|
+
const young = age < YOUNG_LOCK_GRACE_MS && age >= -3e5;
|
|
2126
|
+
return young ? { kind: "young" } : { kind: "orphan", tokens, raw };
|
|
2127
|
+
}
|
|
2128
|
+
async function isStale(meta, staleMs, cache4) {
|
|
2129
|
+
const age = Date.now() - meta.started;
|
|
2130
|
+
if (age > staleMs || age < -3e5) return true;
|
|
2131
|
+
if (meta.hostname && meta.hostname !== hostname()) return false;
|
|
2132
|
+
if (!isProcessAlive(meta.pid)) return true;
|
|
2133
|
+
if (meta.procStart === void 0 || age < PID_REUSE_PROBE_AFTER_MS) return false;
|
|
2134
|
+
return pidReused(meta.pid, meta.procStart, cache4);
|
|
2135
|
+
}
|
|
2136
|
+
function describeHolder(state) {
|
|
2137
|
+
if (state.kind !== "held" && state.kind !== "legacy") return "unknown (unreadable lock metadata)";
|
|
2138
|
+
const { meta } = state;
|
|
2139
|
+
const host = meta.hostname ? `${meta.hostname}:` : "";
|
|
2140
|
+
return `${host}pid ${meta.pid} (running ${Math.max(0, Date.now() - meta.started)}ms)`;
|
|
2141
|
+
}
|
|
2142
|
+
async function dirAgeMs(lockPath) {
|
|
2143
|
+
try {
|
|
2144
|
+
return Date.now() - (await stat(lockPath)).mtimeMs;
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
if (errorCode(err) === "ENOENT") return null;
|
|
2147
|
+
throw err;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function pidReused(pid, recorded, cache4) {
|
|
2151
|
+
const key = `${pid}:${recorded}`;
|
|
2152
|
+
let verdict = cache4.get(key);
|
|
2153
|
+
if (!verdict) {
|
|
2154
|
+
verdict = processIdentity(pid).then((current) => current !== null && current !== recorded);
|
|
2155
|
+
cache4.set(key, verdict);
|
|
2156
|
+
}
|
|
2157
|
+
return verdict;
|
|
2158
|
+
}
|
|
2159
|
+
function isProcessAlive(pid) {
|
|
2160
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2161
|
+
try {
|
|
2162
|
+
process.kill(pid, 0);
|
|
2163
|
+
return true;
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
return errorCode(err) === "EPERM";
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
function parseMetadata(raw) {
|
|
2169
|
+
let value;
|
|
2170
|
+
try {
|
|
2171
|
+
value = JSON.parse(raw);
|
|
2172
|
+
} catch {
|
|
2173
|
+
return null;
|
|
2174
|
+
}
|
|
2175
|
+
if (typeof value !== "object" || value === null) return null;
|
|
2176
|
+
const v = value;
|
|
2177
|
+
if (typeof v.pid !== "number" || typeof v.started !== "number") return null;
|
|
2178
|
+
const optionalText = (x) => x === void 0 || typeof x === "string";
|
|
2179
|
+
const textOk = [v.hostname, v.token, v.procStart].every(optionalText);
|
|
2180
|
+
return textOk ? value : null;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// src/utils/filesystem/process-lock-ops.ts
|
|
2184
|
+
async function tryAcquire(lockPath, meta) {
|
|
2185
|
+
try {
|
|
2186
|
+
await mkdir(lockPath);
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
if (errorCode(err) === "EEXIST") return false;
|
|
2189
|
+
throw err;
|
|
2190
|
+
}
|
|
2191
|
+
const owner = ownerPath(lockPath, meta.token);
|
|
2192
|
+
let writingHolder = false;
|
|
2193
|
+
try {
|
|
2194
|
+
await mkdir(owner);
|
|
2195
|
+
if ((await ownerTokens(lockPath))?.length !== 1) return await backOff(lockPath, owner);
|
|
2196
|
+
writingHolder = true;
|
|
2197
|
+
await writeFile(holderPath(lockPath), JSON.stringify(meta), { encoding: "utf-8", flag: "wx" });
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
const code = errorCode(err);
|
|
2200
|
+
if (code === "ENOENT" || code === "EEXIST") return backOff(lockPath, owner);
|
|
2201
|
+
if (writingHolder) await rm(holderPath(lockPath), { force: true }).catch(() => {
|
|
2202
|
+
});
|
|
2203
|
+
await backOff(lockPath, owner);
|
|
2204
|
+
throw err;
|
|
2205
|
+
}
|
|
2206
|
+
if (existsSync(owner)) return true;
|
|
2207
|
+
await teardown(lockPath, [meta.token]);
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
function releaseOwnedSync(lockPath, token) {
|
|
2211
|
+
try {
|
|
2212
|
+
rmdirSync(ownerPath(lockPath, token));
|
|
2213
|
+
} catch {
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
if (holderToken(readFileSync(holderPath(lockPath), "utf-8")) === token) {
|
|
2218
|
+
unlinkSync(holderPath(lockPath));
|
|
2219
|
+
}
|
|
2220
|
+
} catch {
|
|
2221
|
+
}
|
|
2222
|
+
try {
|
|
2223
|
+
rmdirSync(lockPath);
|
|
2224
|
+
} catch {
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async function evict(lockPath, state) {
|
|
2228
|
+
if (state.kind === "held") return evictOwners(lockPath, [state.token]);
|
|
2229
|
+
if (state.kind === "orphan" && state.tokens.length > 0) {
|
|
2230
|
+
return evictOwners(lockPath, state.tokens);
|
|
2231
|
+
}
|
|
2232
|
+
if (state.kind === "legacy" || state.kind === "orphan") return dropUnowned(lockPath, state.raw);
|
|
2233
|
+
}
|
|
2234
|
+
async function evictOwners(lockPath, tokens) {
|
|
2235
|
+
const removed = [];
|
|
2236
|
+
for (const token of tokens) {
|
|
2237
|
+
if (await removeOwner(lockPath, token)) removed.push(token);
|
|
2238
|
+
}
|
|
2239
|
+
if (removed.length > 0) await teardown(lockPath, removed);
|
|
2240
|
+
}
|
|
2241
|
+
async function removeOwner(lockPath, token) {
|
|
2242
|
+
try {
|
|
2243
|
+
await retryTransient(() => rmdir(ownerPath(lockPath, token)));
|
|
2244
|
+
return true;
|
|
2245
|
+
} catch (err) {
|
|
2246
|
+
if (errorCode(err) === "ENOENT") return false;
|
|
2247
|
+
throw err;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
async function teardown(lockPath, tokens) {
|
|
2251
|
+
const token = holderToken(await readHolderRaw(lockPath));
|
|
2252
|
+
if (token !== void 0 && tokens.includes(token)) {
|
|
2253
|
+
await unlink(holderPath(lockPath)).catch(() => {
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
await rmdir(lockPath).catch(() => {
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
async function dropUnowned(lockPath, judgedRaw) {
|
|
2260
|
+
const aside = `${lockPath}.${randomUUID()}.stale`;
|
|
2261
|
+
try {
|
|
2262
|
+
await renameWithRetry(lockPath, aside);
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
if (errorCode(err) === "ENOENT") return;
|
|
2265
|
+
throw err;
|
|
2266
|
+
}
|
|
2267
|
+
const owners = await ownerTokens(aside);
|
|
2268
|
+
if (owners?.length !== 0 || await readHolderRaw(aside) !== judgedRaw) {
|
|
2269
|
+
return putBack(aside, lockPath);
|
|
2270
|
+
}
|
|
2271
|
+
try {
|
|
2272
|
+
await rm(aside, { recursive: true, force: true });
|
|
2273
|
+
} catch (err) {
|
|
2274
|
+
await putBack(aside, lockPath);
|
|
2275
|
+
throw err;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
async function putBack(aside, lockPath) {
|
|
2279
|
+
await rename(aside, lockPath).catch(() => {
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
async function backOff(lockPath, owner) {
|
|
2283
|
+
await rmdir(owner).catch(() => {
|
|
2284
|
+
});
|
|
2285
|
+
await rmdir(lockPath).catch(() => {
|
|
2286
|
+
});
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
1172
2289
|
|
|
1173
2290
|
// src/utils/filesystem/process-lock.ts
|
|
1174
2291
|
var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
1175
2292
|
var DEFAULT_RETRIES = 30;
|
|
1176
2293
|
var DEFAULT_RETRY_DELAY_MS = 200;
|
|
1177
|
-
var
|
|
2294
|
+
var MAX_IMMEDIATE_RETRIES = 100;
|
|
2295
|
+
var MAX_TRANSIENT_ERRORS = 5;
|
|
1178
2296
|
async function acquireProcessLock(lockPath, opts = {}) {
|
|
1179
2297
|
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
1180
|
-
const
|
|
1181
|
-
const stale = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
2298
|
+
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
1182
2299
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2300
|
+
const procStart = await selfIdentity();
|
|
2301
|
+
const probes = /* @__PURE__ */ new Map();
|
|
1183
2302
|
let attempt = 0;
|
|
2303
|
+
let immediate = 0;
|
|
2304
|
+
const waitingSince = Date.now();
|
|
2305
|
+
let noticed = false;
|
|
2306
|
+
let transient = 0;
|
|
1184
2307
|
while (true) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
await
|
|
2308
|
+
let state;
|
|
2309
|
+
try {
|
|
2310
|
+
const holder = newHolder(procStart);
|
|
2311
|
+
if (await tryAcquire(lockPath, holder)) return holdLock(lockPath, holder.token);
|
|
2312
|
+
state = await inspectLock(lockPath);
|
|
2313
|
+
transient = 0;
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
if (!isTransientFsError(err) || ++transient >= MAX_TRANSIENT_ERRORS) throw err;
|
|
2316
|
+
await setTimeout(lockRetryDelayMs(transient, opts));
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (immediate < MAX_IMMEDIATE_RETRIES && await clearedNow(lockPath, state, staleMs, probes)) {
|
|
2320
|
+
immediate++;
|
|
1190
2321
|
continue;
|
|
1191
2322
|
}
|
|
1192
2323
|
if (attempt >= retries) {
|
|
1193
|
-
|
|
1194
|
-
throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
|
|
2324
|
+
throw new LockAcquisitionError(lockPath, describeHolder(state), { label: opts.label });
|
|
1195
2325
|
}
|
|
1196
2326
|
attempt++;
|
|
1197
|
-
|
|
2327
|
+
immediate = 0;
|
|
2328
|
+
if (!noticed && opts.onWait && Date.now() - waitingSince >= (opts.waitNoticeMs ?? 2e3)) {
|
|
2329
|
+
noticed = true;
|
|
2330
|
+
opts.onWait(describeHolder(state));
|
|
2331
|
+
}
|
|
2332
|
+
await setTimeout(lockRetryDelayMs(attempt, opts));
|
|
1198
2333
|
}
|
|
1199
2334
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
2335
|
+
function lockRetryDelayMs(attempt, opts, random = Math.random) {
|
|
2336
|
+
const base = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
2337
|
+
const cap = Math.max(base, opts.maxRetryDelayMs ?? base);
|
|
2338
|
+
const delay = Math.min(cap, base * 2 ** (attempt - 1));
|
|
2339
|
+
return opts.jitter ? delay * (0.5 + random() * 0.5) : delay;
|
|
2340
|
+
}
|
|
2341
|
+
async function clearedNow(lockPath, state, staleMs, probes) {
|
|
2342
|
+
if (state.kind === "gone") return true;
|
|
2343
|
+
if (state.kind === "young") return false;
|
|
2344
|
+
if (state.kind !== "orphan" && !await isStale(state.meta, staleMs, probes)) return false;
|
|
2345
|
+
await evict(lockPath, state);
|
|
2346
|
+
return true;
|
|
2347
|
+
}
|
|
2348
|
+
function newHolder(procStart) {
|
|
2349
|
+
return {
|
|
1209
2350
|
pid: process.pid,
|
|
1210
2351
|
started: Date.now(),
|
|
1211
|
-
hostname:
|
|
2352
|
+
hostname: hostname(),
|
|
2353
|
+
token: randomUUID(),
|
|
2354
|
+
...procStart === null ? {} : { procStart }
|
|
1212
2355
|
};
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
} catch (error) {
|
|
1216
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
1217
|
-
});
|
|
1218
|
-
throw error;
|
|
1219
|
-
}
|
|
2356
|
+
}
|
|
2357
|
+
function holdLock(lockPath, token) {
|
|
1220
2358
|
let released = false;
|
|
1221
2359
|
const cleanup = () => {
|
|
1222
2360
|
if (released) return;
|
|
1223
2361
|
released = true;
|
|
1224
|
-
|
|
1225
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
1226
|
-
} catch {
|
|
1227
|
-
}
|
|
2362
|
+
releaseOwnedSync(lockPath, token);
|
|
1228
2363
|
};
|
|
1229
2364
|
const signalHandler = (signal) => {
|
|
1230
2365
|
cleanup();
|
|
@@ -1233,70 +2368,109 @@ async function tryAcquire(lockPath) {
|
|
|
1233
2368
|
process.once("SIGINT", signalHandler);
|
|
1234
2369
|
process.once("SIGTERM", signalHandler);
|
|
1235
2370
|
process.once("exit", cleanup);
|
|
1236
|
-
|
|
2371
|
+
const release = async () => {
|
|
1237
2372
|
if (released) return;
|
|
1238
2373
|
released = true;
|
|
1239
2374
|
process.off("SIGINT", signalHandler);
|
|
1240
2375
|
process.off("SIGTERM", signalHandler);
|
|
1241
2376
|
process.off("exit", cleanup);
|
|
1242
|
-
await
|
|
2377
|
+
await evictOwners(lockPath, [token]).catch(() => {
|
|
1243
2378
|
});
|
|
1244
2379
|
};
|
|
2380
|
+
const isHeld = async () => !released && existsSync(ownerPath(lockPath, token));
|
|
2381
|
+
return Object.assign(release, { isHeld });
|
|
1245
2382
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
function isStale(meta, staleMs) {
|
|
1263
|
-
if (!meta) return true;
|
|
1264
|
-
const sameHost = !meta.hostname || meta.hostname === getHostname();
|
|
1265
|
-
if (sameHost && !isProcessAlive(meta.pid)) return true;
|
|
1266
|
-
return Date.now() - meta.started > staleMs;
|
|
2383
|
+
|
|
2384
|
+
// src/utils/output/color.ts
|
|
2385
|
+
function noColorRequested() {
|
|
2386
|
+
const value = process.env.NO_COLOR;
|
|
2387
|
+
return value !== void 0 && value !== "";
|
|
2388
|
+
}
|
|
2389
|
+
function forceColorRequested() {
|
|
2390
|
+
const value = process.env.FORCE_COLOR;
|
|
2391
|
+
if (value === void 0) return void 0;
|
|
2392
|
+
return value !== "0" && value !== "false";
|
|
2393
|
+
}
|
|
2394
|
+
function colorEnabled(stream = process.stdout) {
|
|
2395
|
+
const forced = forceColorRequested();
|
|
2396
|
+
if (forced !== void 0) return forced;
|
|
2397
|
+
if (noColorRequested()) return false;
|
|
2398
|
+
return stream.isTTY === true;
|
|
1267
2399
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
2400
|
+
|
|
2401
|
+
// src/utils/output/logger.ts
|
|
2402
|
+
var C = {
|
|
2403
|
+
green: "\x1B[32m",
|
|
2404
|
+
red: "\x1B[31m",
|
|
2405
|
+
yellow: "\x1B[33m",
|
|
2406
|
+
cyan: "\x1B[36m",
|
|
2407
|
+
reset: "\x1B[0m"
|
|
2408
|
+
};
|
|
2409
|
+
function outStream() {
|
|
2410
|
+
return process.stdout;
|
|
2411
|
+
}
|
|
2412
|
+
function out(text) {
|
|
2413
|
+
outStream().write(text);
|
|
2414
|
+
}
|
|
2415
|
+
function c(code, text, stream) {
|
|
2416
|
+
return colorEnabled(stream) ? `${code}${text}${C.reset}` : text;
|
|
2417
|
+
}
|
|
2418
|
+
var logger = {
|
|
2419
|
+
info(msg) {
|
|
2420
|
+
out(c(C.cyan, msg, outStream()) + "\n");
|
|
2421
|
+
},
|
|
2422
|
+
warn(msg) {
|
|
2423
|
+
process.stderr.write(c(C.yellow, "\u26A0 ", process.stderr) + msg + "\n");
|
|
2424
|
+
},
|
|
2425
|
+
error(msg) {
|
|
2426
|
+
process.stderr.write(c(C.red, "\u2717 ", process.stderr) + msg + "\n");
|
|
2427
|
+
},
|
|
2428
|
+
success(msg) {
|
|
2429
|
+
out(c(C.green, "\u2713 ", outStream()) + msg + "\n");
|
|
2430
|
+
},
|
|
2431
|
+
debug(msg) {
|
|
2432
|
+
if (process.env.AGENTSMESH_DEBUG === "1") {
|
|
2433
|
+
out(c(C.cyan, "[debug] ", outStream()) + msg + "\n");
|
|
2434
|
+
}
|
|
1275
2435
|
}
|
|
1276
|
-
}
|
|
1277
|
-
function describeHolder(meta) {
|
|
1278
|
-
if (!meta) return "unknown (unreadable lock metadata)";
|
|
1279
|
-
const host = meta.hostname ? `${meta.hostname}:` : "";
|
|
1280
|
-
return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
|
|
1281
|
-
}
|
|
1282
|
-
function isLockMetadata(value) {
|
|
1283
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1284
|
-
const v = value;
|
|
1285
|
-
return typeof v.pid === "number" && typeof v.started === "number";
|
|
1286
|
-
}
|
|
1287
|
-
function getHostname() {
|
|
1288
|
-
return hostname();
|
|
1289
|
-
}
|
|
2436
|
+
};
|
|
1290
2437
|
|
|
1291
2438
|
// src/lessons/lessons-lock.ts
|
|
1292
2439
|
var LESSONS_LOCK_FILENAME = ".lessons.lock";
|
|
2440
|
+
var LESSONS_LOCK_OPTIONS = Object.freeze({
|
|
2441
|
+
retries: 500,
|
|
2442
|
+
retryDelayMs: 25,
|
|
2443
|
+
maxRetryDelayMs: 250,
|
|
2444
|
+
jitter: true,
|
|
2445
|
+
staleMs: 6e4
|
|
2446
|
+
});
|
|
1293
2447
|
function lessonsLockPath(projectRoot) {
|
|
1294
2448
|
return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
|
|
1295
2449
|
}
|
|
1296
2450
|
async function acquireLessonsLock(projectRoot, opts = {}) {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
2451
|
+
return acquireProcessLock(lessonsLockPath(projectRoot), {
|
|
2452
|
+
retries: opts.retries ?? LESSONS_LOCK_OPTIONS.retries,
|
|
2453
|
+
retryDelayMs: opts.retryDelayMs ?? LESSONS_LOCK_OPTIONS.retryDelayMs,
|
|
2454
|
+
maxRetryDelayMs: opts.maxRetryDelayMs ?? LESSONS_LOCK_OPTIONS.maxRetryDelayMs,
|
|
2455
|
+
jitter: opts.jitter ?? LESSONS_LOCK_OPTIONS.jitter,
|
|
2456
|
+
staleMs: opts.staleMs ?? LESSONS_LOCK_OPTIONS.staleMs,
|
|
2457
|
+
label: "lessons lock",
|
|
2458
|
+
waitNoticeMs: opts.waitNoticeMs,
|
|
2459
|
+
onWait: opts.onWait ?? ((holder) => logger.warn(
|
|
2460
|
+
`Waiting for the lessons lock, held by ${holder}; a lock older than ${LESSONS_LOCK_OPTIONS.staleMs / 1e3} s is taken over.`
|
|
2461
|
+
))
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
var LessonsLockLostError = class extends Error {
|
|
2465
|
+
constructor() {
|
|
2466
|
+
super(
|
|
2467
|
+
`lost the lessons lock while writing (the process was paused longer than the ${LESSONS_LOCK_OPTIONS.staleMs / 1e3} s stale window?); nothing was saved \u2014 retry the command`
|
|
2468
|
+
);
|
|
2469
|
+
this.name = "LessonsLockLostError";
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
async function assertLessonsLockHeld(lock) {
|
|
2473
|
+
if (!await lock.isHeld()) throw new LessonsLockLostError();
|
|
1300
2474
|
}
|
|
1301
2475
|
|
|
1302
2476
|
// src/lessons/validate-checks.ts
|
|
@@ -1500,6 +2674,8 @@ function collectDuplicateRules(graph, findings) {
|
|
|
1500
2674
|
}
|
|
1501
2675
|
function collectInvalidTriggerPatterns(graph, findings) {
|
|
1502
2676
|
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
2677
|
+
const globFinding = unsafeGlobFinding(triggerId, trigger);
|
|
2678
|
+
if (globFinding !== null) findings.push(globFinding);
|
|
1503
2679
|
if (trigger.kind !== "command_pattern") continue;
|
|
1504
2680
|
try {
|
|
1505
2681
|
new RegExp(trigger.pattern);
|
|
@@ -1507,7 +2683,7 @@ function collectInvalidTriggerPatterns(graph, findings) {
|
|
|
1507
2683
|
findings.push({
|
|
1508
2684
|
level: "error",
|
|
1509
2685
|
code: "INVALID_TRIGGER_PATTERN",
|
|
1510
|
-
message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${
|
|
2686
|
+
message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${regexSyntaxReason(err)}.`,
|
|
1511
2687
|
triggerId
|
|
1512
2688
|
});
|
|
1513
2689
|
continue;
|
|
@@ -1516,12 +2692,16 @@ function collectInvalidTriggerPatterns(graph, findings) {
|
|
|
1516
2692
|
findings.push({
|
|
1517
2693
|
level: "error",
|
|
1518
2694
|
code: "UNSAFE_TRIGGER_PATTERN",
|
|
1519
|
-
message: `Trigger "${triggerId}" has a command_pattern regex
|
|
2695
|
+
message: `Trigger "${triggerId}" has a command_pattern regex the linear matcher cannot run (${trigger.pattern}). It does not support backreferences (\\1, \\k<name>), lookarounds ((?=x), (?!x), (?<=x), (?<!x)), or patterns too large to run, such as (a{1000}){10}. Nested quantifiers such as (a+)+ are fine. Rewrite the pattern without the unsupported part.`,
|
|
1520
2696
|
triggerId
|
|
1521
2697
|
});
|
|
1522
2698
|
}
|
|
1523
2699
|
}
|
|
1524
2700
|
}
|
|
2701
|
+
function regexSyntaxReason(err) {
|
|
2702
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
2703
|
+
return text.replace(/^Invalid regular expression: \/[\s\S]*\/[a-z]*: /, "");
|
|
2704
|
+
}
|
|
1525
2705
|
function collectBackslashGlobPatterns(graph, findings) {
|
|
1526
2706
|
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1527
2707
|
if (trigger.kind !== "file_glob") continue;
|
|
@@ -1620,7 +2800,7 @@ function collectStopwordKeywords(graph, findings) {
|
|
|
1620
2800
|
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1621
2801
|
if (trigger.kind !== "keyword") continue;
|
|
1622
2802
|
if (!active.has(triggerId)) continue;
|
|
1623
|
-
if (
|
|
2803
|
+
if (tokenize2(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
|
|
1624
2804
|
continue;
|
|
1625
2805
|
}
|
|
1626
2806
|
findings.push({
|
|
@@ -1667,9 +2847,17 @@ function validateLessonsGraph(graph, options = {}) {
|
|
|
1667
2847
|
}
|
|
1668
2848
|
|
|
1669
2849
|
// src/lessons/mutate.ts
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2850
|
+
var LessonsWriteRefusedError = class extends Error {
|
|
2851
|
+
findings;
|
|
2852
|
+
constructor(findings) {
|
|
2853
|
+
const errors = findings.map((f) => `${f.code}: ${f.message.replace(/[.\s]+$/, "")}`).join("; ");
|
|
2854
|
+
super(
|
|
2855
|
+
`Refused to save the lessons graph: this change would add ${errors}. Nothing was written.`
|
|
2856
|
+
);
|
|
2857
|
+
this.name = "LessonsWriteRefusedError";
|
|
2858
|
+
this.findings = findings;
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
1673
2861
|
function findingKey(f) {
|
|
1674
2862
|
return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
|
|
1675
2863
|
}
|
|
@@ -1679,6 +2867,7 @@ function errorSignatures(report) {
|
|
|
1679
2867
|
async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
|
|
1680
2868
|
const release = await acquireLessonsLock(projectRoot, { retries: options.retries });
|
|
1681
2869
|
try {
|
|
2870
|
+
sweepLessonsLeftovers(projectRoot);
|
|
1682
2871
|
const graph = tryLoadLessonsGraph(projectRoot) ?? emptyGraph();
|
|
1683
2872
|
const baseline = errorSignatures(validateLessonsGraph(graph));
|
|
1684
2873
|
const result = await mutator(graph);
|
|
@@ -1687,12 +2876,10 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
|
|
|
1687
2876
|
(f) => f.level === "error" && !baseline.has(findingKey(f))
|
|
1688
2877
|
);
|
|
1689
2878
|
if (introduced.length > 0) {
|
|
1690
|
-
|
|
1691
|
-
throw new Error(
|
|
1692
|
-
`mutateLessonsGraph: refusing to write \u2014 this change introduces ${errors}. (Pre-existing graph issues are not blocking; run \`agentsmesh lessons validate\` to review and \`lessons untrigger\`/\`prune\` to repair them.)`
|
|
1693
|
-
);
|
|
2879
|
+
throw new LessonsWriteRefusedError(introduced);
|
|
1694
2880
|
}
|
|
1695
2881
|
graph.version = CURRENT_GRAPH_VERSION;
|
|
2882
|
+
await assertLessonsLockHeld(release);
|
|
1696
2883
|
saveLessonsGraph(projectRoot, graph);
|
|
1697
2884
|
return result;
|
|
1698
2885
|
} finally {
|
|
@@ -1706,50 +2893,41 @@ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
|
|
|
1706
2893
|
|
|
1707
2894
|
// src/lessons/add.ts
|
|
1708
2895
|
async function addLesson(projectRoot, input, options = {}) {
|
|
1709
|
-
return mutateLessonsGraph(
|
|
1710
|
-
|
|
1711
|
-
|
|
2896
|
+
return mutateLessonsGraph(
|
|
2897
|
+
projectRoot,
|
|
2898
|
+
(graph) => addLessonInto(graph, input, { ...options, projectRoot }),
|
|
2899
|
+
{ retries: options.retries }
|
|
2900
|
+
);
|
|
1712
2901
|
}
|
|
1713
2902
|
function addLessonInto(graph, input, options) {
|
|
1714
2903
|
const ruleKey = normalizeRule(input.rule);
|
|
1715
2904
|
const trimmedRule = assertRuleShape(input.rule);
|
|
1716
2905
|
const existingId = findExistingLessonByRule(graph, ruleKey);
|
|
1717
|
-
const isNewTopic = graph
|
|
1718
|
-
if (isNewTopic) {
|
|
1719
|
-
if (options.allowNewTopic !== true) throw new UnknownTopicError(input.topic);
|
|
1720
|
-
if (options.topicSummary === void 0 || options.topicSummary.length === 0) {
|
|
1721
|
-
throw new Error(`addLesson: new topic "${input.topic}" requires topicSummary.`);
|
|
1722
|
-
}
|
|
1723
|
-
graph.topics[input.topic] = { summary: options.topicSummary };
|
|
1724
|
-
}
|
|
2906
|
+
const isNewTopic = ensureTopic(graph, input.topic, options);
|
|
1725
2907
|
const existing = existingId !== null ? graph.lessons[existingId] : void 0;
|
|
1726
2908
|
assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
|
|
1727
|
-
const
|
|
2909
|
+
const merged = mergeTriggers(graph, input.triggers, options.projectRoot);
|
|
2910
|
+
const { triggerIds, newTriggerIds, dropped } = dropDeadCommandTriggers(graph, merged, options);
|
|
1728
2911
|
if (!skipsTriggerGates(input, options)) {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
|
|
1732
|
-
);
|
|
2912
|
+
const resulting = existing === void 0 ? triggerIds : union(existing.triggers, triggerIds);
|
|
2913
|
+
assertRecallable(graph, resulting, dropped);
|
|
1733
2914
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
topics: union(existing2.topics, [input.topic]),
|
|
1739
|
-
triggers: union(existing2.triggers, triggerIds),
|
|
1740
|
-
evidence: union(existing2.evidence, input.evidence ?? []),
|
|
1741
|
-
...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
|
|
1742
|
-
// Re-capturing a rule with --scope always promotes it to always-on.
|
|
1743
|
-
...input.scope === "always" ? { scope: "always" } : {}
|
|
1744
|
-
};
|
|
2915
|
+
const droppedWarnings = dropped.map(deadCommandWarning);
|
|
2916
|
+
if (existingId !== null && existing !== void 0) {
|
|
2917
|
+
const updated = upsertLesson(existing, input, triggerIds);
|
|
2918
|
+
graph.lessons[existingId] = updated;
|
|
1745
2919
|
return {
|
|
1746
2920
|
id: existingId,
|
|
1747
2921
|
isNewLesson: false,
|
|
1748
2922
|
isNewTopic,
|
|
1749
2923
|
newTriggerIds,
|
|
2924
|
+
changes: describeUpsert(existing, updated),
|
|
1750
2925
|
// Near-duplicate detection is meaningless on an upsert (the lesson IS the
|
|
1751
2926
|
// match), so only DEAD_GLOB/hygiene warnings apply here.
|
|
1752
|
-
warnings:
|
|
2927
|
+
warnings: [
|
|
2928
|
+
...inspectCapturedLesson(graph, existingId, options.knownPaths),
|
|
2929
|
+
...droppedWarnings
|
|
2930
|
+
]
|
|
1753
2931
|
};
|
|
1754
2932
|
}
|
|
1755
2933
|
const id = makeLessonId(graph, input.topic, ruleKey);
|
|
@@ -1757,29 +2935,26 @@ function addLessonInto(graph, input, options) {
|
|
|
1757
2935
|
rule: trimmedRule,
|
|
1758
2936
|
topics: [input.topic],
|
|
1759
2937
|
triggers: triggerIds,
|
|
1760
|
-
evidence: input.evidence
|
|
2938
|
+
evidence: [...new Set(input.evidence ?? [])],
|
|
1761
2939
|
status: "active",
|
|
1762
2940
|
createdAt: input.createdAt ?? todayIso(),
|
|
1763
2941
|
...input.rationale === void 0 ? {} : { rationale: input.rationale },
|
|
1764
2942
|
...input.scope === "always" ? { scope: "always" } : {}
|
|
1765
2943
|
};
|
|
1766
|
-
const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
|
|
1767
2944
|
const nearDup = nearDuplicateWarning(graph, id);
|
|
1768
2945
|
return {
|
|
1769
2946
|
id,
|
|
1770
2947
|
isNewLesson: true,
|
|
1771
2948
|
isNewTopic,
|
|
1772
2949
|
newTriggerIds,
|
|
1773
|
-
|
|
2950
|
+
changes: [],
|
|
2951
|
+
warnings: [
|
|
2952
|
+
...inspectCapturedLesson(graph, id, options.knownPaths),
|
|
2953
|
+
...nearDup === null ? [] : [nearDup],
|
|
2954
|
+
...droppedWarnings
|
|
2955
|
+
]
|
|
1774
2956
|
};
|
|
1775
2957
|
}
|
|
1776
|
-
function findExistingLessonByRule(graph, ruleKey) {
|
|
1777
|
-
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1778
|
-
if (lesson.status !== "active") continue;
|
|
1779
|
-
if (normalizeRule(lesson.rule) === ruleKey) return id;
|
|
1780
|
-
}
|
|
1781
|
-
return null;
|
|
1782
|
-
}
|
|
1783
2958
|
|
|
1784
2959
|
// src/lessons/import-legacy-merge.ts
|
|
1785
2960
|
async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
|
|
@@ -1812,43 +2987,74 @@ async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
|
|
|
1812
2987
|
triggerCount: addedTriggers.size
|
|
1813
2988
|
};
|
|
1814
2989
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
topicsDir: join(base, "topics")
|
|
1825
|
-
};
|
|
2990
|
+
async function canonicalizePath(path) {
|
|
2991
|
+
try {
|
|
2992
|
+
return await realpath(path);
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
if (error.code !== "ENOENT") throw error;
|
|
2995
|
+
const parent = dirname(path);
|
|
2996
|
+
if (parent === path) return resolve(path);
|
|
2997
|
+
return join(await canonicalizePath(parent), basename(path));
|
|
2998
|
+
}
|
|
1826
2999
|
}
|
|
1827
|
-
function
|
|
1828
|
-
return
|
|
3000
|
+
function isPathInside(target, root) {
|
|
3001
|
+
return target === root || target.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
|
|
3002
|
+
}
|
|
3003
|
+
var display = (path) => path.replaceAll("\\", "/");
|
|
3004
|
+
async function assertPathInsideRoot(root, target) {
|
|
3005
|
+
const rootAbs = resolve(root);
|
|
3006
|
+
const targetAbs = resolve(target);
|
|
3007
|
+
if (!isPathInside(targetAbs, rootAbs)) {
|
|
3008
|
+
throw new Error(`Unsafe filesystem path: ${display(target)} is outside ${display(rootAbs)}`);
|
|
3009
|
+
}
|
|
3010
|
+
let realTarget;
|
|
3011
|
+
let realRoot;
|
|
3012
|
+
try {
|
|
3013
|
+
[realTarget, realRoot] = await Promise.all([
|
|
3014
|
+
canonicalizePath(targetAbs),
|
|
3015
|
+
canonicalizePath(rootAbs)
|
|
3016
|
+
]);
|
|
3017
|
+
} catch (cause) {
|
|
3018
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
3019
|
+
throw new Error(
|
|
3020
|
+
`Unsafe filesystem path: ${display(target)} could not be resolved (${detail})`,
|
|
3021
|
+
{ cause }
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
if (isPathInside(realTarget, realRoot)) return;
|
|
3025
|
+
throw new Error(
|
|
3026
|
+
`Unsafe filesystem path: ${display(target)} resolves to ${display(realTarget)} outside ${display(realRoot)}`
|
|
3027
|
+
);
|
|
1829
3028
|
}
|
|
1830
|
-
var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
1831
|
-
|
|
1832
|
-
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
1833
|
-
|
|
1834
|
-
**Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command> --session auto\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always --session auto\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
|
|
1835
|
-
|
|
1836
|
-
**Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
|
|
1837
|
-
|
|
1838
|
-
**Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
|
|
1839
3029
|
|
|
1840
|
-
// src/lessons/import-legacy.ts
|
|
1841
|
-
var
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
3030
|
+
// src/lessons/import-legacy-read.ts
|
|
3031
|
+
var LESSONS_DIR = ".agentsmesh/lessons";
|
|
3032
|
+
var LegacyTopicPathError = class extends Error {
|
|
3033
|
+
code = "LEGACY_TOPIC_PATH_OUTSIDE";
|
|
3034
|
+
constructor(file) {
|
|
3035
|
+
super(
|
|
3036
|
+
`Legacy topic file path is outside .agentsmesh/lessons/: ${file}. Refusing to migrate (legacy artifacts left intact).`
|
|
3037
|
+
);
|
|
3038
|
+
this.name = "LegacyTopicPathError";
|
|
1846
3039
|
}
|
|
1847
3040
|
};
|
|
1848
|
-
async function
|
|
1849
|
-
const
|
|
1850
|
-
const
|
|
1851
|
-
const
|
|
3041
|
+
async function resolveLegacyTopicPath(projectRoot, file) {
|
|
3042
|
+
const forward = file.replaceAll("\\", "/");
|
|
3043
|
+
const normalized = posix.normalize(forward);
|
|
3044
|
+
const relative3 = !/^[A-Za-z]:/.test(forward) && !forward.startsWith("/") && normalized.startsWith(`${LESSONS_DIR}/`);
|
|
3045
|
+
if (!relative3) throw new LegacyTopicPathError(file);
|
|
3046
|
+
const target = join(projectRoot, normalized);
|
|
3047
|
+
try {
|
|
3048
|
+
await assertPathInsideRoot(join(projectRoot, LESSONS_DIR), target);
|
|
3049
|
+
} catch {
|
|
3050
|
+
throw new LegacyTopicPathError(file);
|
|
3051
|
+
}
|
|
3052
|
+
return target;
|
|
3053
|
+
}
|
|
3054
|
+
async function readLegacySource(projectRoot, migratedAt) {
|
|
3055
|
+
const index = LegacyIndexSchema.parse(
|
|
3056
|
+
parse(readFileSync(lessonsPaths(projectRoot).index, "utf8"))
|
|
3057
|
+
);
|
|
1852
3058
|
const topics = {};
|
|
1853
3059
|
const triggersById = /* @__PURE__ */ new Map();
|
|
1854
3060
|
const triggerIdByKey = /* @__PURE__ */ new Map();
|
|
@@ -1859,14 +3065,15 @@ async function importLegacyLessons(projectRoot, options) {
|
|
|
1859
3065
|
topics[cluster.topic] = { summary: cluster.summary };
|
|
1860
3066
|
summaryByTopic.set(cluster.topic, cluster.summary);
|
|
1861
3067
|
const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
|
|
1862
|
-
const topicFile =
|
|
3068
|
+
const topicFile = await resolveLegacyTopicPath(projectRoot, cluster.file);
|
|
1863
3069
|
if (!existsSync(topicFile)) {
|
|
1864
3070
|
throw new Error(
|
|
1865
|
-
`
|
|
3071
|
+
`Legacy topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
|
|
1866
3072
|
);
|
|
1867
3073
|
}
|
|
1868
|
-
const
|
|
1869
|
-
|
|
3074
|
+
for (const { index: ruleIndex, body, evidence } of parseRulesSection(
|
|
3075
|
+
readFileSync(topicFile, "utf8")
|
|
3076
|
+
)) {
|
|
1870
3077
|
const lessonEvidence = [
|
|
1871
3078
|
`legacy:${cluster.file}#rule-${ruleIndex}`,
|
|
1872
3079
|
...evidence.map((e) => `legacy:${e}`)
|
|
@@ -1877,7 +3084,7 @@ async function importLegacyLessons(projectRoot, options) {
|
|
|
1877
3084
|
triggers: clusterTriggerIds,
|
|
1878
3085
|
evidence: lessonEvidence,
|
|
1879
3086
|
status: "active",
|
|
1880
|
-
createdAt:
|
|
3087
|
+
createdAt: migratedAt
|
|
1881
3088
|
};
|
|
1882
3089
|
specs.push({
|
|
1883
3090
|
rule: body,
|
|
@@ -1888,22 +3095,43 @@ async function importLegacyLessons(projectRoot, options) {
|
|
|
1888
3095
|
keywords: cluster.triggers.keywords
|
|
1889
3096
|
},
|
|
1890
3097
|
evidence: lessonEvidence,
|
|
1891
|
-
createdAt:
|
|
3098
|
+
createdAt: migratedAt
|
|
1892
3099
|
});
|
|
1893
3100
|
}
|
|
1894
3101
|
}
|
|
1895
|
-
|
|
3102
|
+
return { topics, triggers: Object.fromEntries(triggersById), lessons, specs, summaryByTopic };
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// src/lessons/import-legacy.ts
|
|
3106
|
+
var LessonsGraphExistsError = class extends Error {
|
|
3107
|
+
code = "LESSONS_GRAPH_EXISTS";
|
|
3108
|
+
constructor() {
|
|
3109
|
+
super(
|
|
3110
|
+
"A non-empty lessons.json already exists. Pass --force to overwrite it, or --merge to add the legacy lessons to it."
|
|
3111
|
+
);
|
|
3112
|
+
this.name = "LessonsGraphExistsError";
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
async function importLegacyLessons(projectRoot, options) {
|
|
3116
|
+
const paths = lessonsPaths(projectRoot);
|
|
3117
|
+
if (options.merge === true) {
|
|
3118
|
+
const { specs, summaryByTopic } = await readLegacySource(projectRoot, options.migratedAt);
|
|
1896
3119
|
return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
|
|
1897
|
-
|
|
1898
|
-
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
3120
|
+
}
|
|
3121
|
+
const { topics, lessons, triggers } = await mutateLessonsGraphLocked(projectRoot, async (g) => {
|
|
3122
|
+
if (options.requireAbsentGraph === true && existsSync(paths.graph)) {
|
|
3123
|
+
throw new LessonsGraphExistsError();
|
|
3124
|
+
}
|
|
1899
3125
|
const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
|
|
1900
3126
|
if (options.force !== true && populated) {
|
|
1901
3127
|
throw new LessonsGraphExistsError();
|
|
1902
3128
|
}
|
|
3129
|
+
const source = await readLegacySource(projectRoot, options.migratedAt);
|
|
1903
3130
|
g.version = CURRENT_GRAPH_VERSION;
|
|
1904
|
-
g.lessons = lessons;
|
|
1905
|
-
g.topics = topics;
|
|
1906
|
-
g.triggers = triggers;
|
|
3131
|
+
g.lessons = source.lessons;
|
|
3132
|
+
g.topics = source.topics;
|
|
3133
|
+
g.triggers = source.triggers;
|
|
3134
|
+
return source;
|
|
1907
3135
|
});
|
|
1908
3136
|
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
1909
3137
|
return {
|
|
@@ -1911,7 +3139,7 @@ async function importLegacyLessons(projectRoot, options) {
|
|
|
1911
3139
|
deletedPaths,
|
|
1912
3140
|
topicCount: Object.keys(topics).length,
|
|
1913
3141
|
lessonCount: Object.keys(lessons).length,
|
|
1914
|
-
triggerCount:
|
|
3142
|
+
triggerCount: Object.keys(triggers).length
|
|
1915
3143
|
};
|
|
1916
3144
|
}
|
|
1917
3145
|
|
|
@@ -1921,7 +3149,7 @@ async function maybeAutoMigrateLessons(projectRoot) {
|
|
|
1921
3149
|
const paths = lessonsPaths(projectRoot);
|
|
1922
3150
|
if (!existsSync(paths.index)) return false;
|
|
1923
3151
|
try {
|
|
1924
|
-
await importLegacyLessons(projectRoot, { migratedAt: todayIso() });
|
|
3152
|
+
await importLegacyLessons(projectRoot, { migratedAt: todayIso(), requireAbsentGraph: true });
|
|
1925
3153
|
return true;
|
|
1926
3154
|
} catch (err) {
|
|
1927
3155
|
if (err instanceof LessonsGraphExistsError) return false;
|
|
@@ -1973,10 +3201,53 @@ function writeSeenStore(path, data, lastAt) {
|
|
|
1973
3201
|
const body = data instanceof Map ? JSON.stringify({ v: 2, lastAt: lastAt ?? Date.now(), seen: Object.fromEntries(data) }) : JSON.stringify(data);
|
|
1974
3202
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
1975
3203
|
writeFileSync(tmp, body, "utf8");
|
|
1976
|
-
renameSync(tmp, path);
|
|
3204
|
+
retryTransientSync(() => renameSync(tmp, path));
|
|
1977
3205
|
} catch {
|
|
1978
3206
|
}
|
|
1979
3207
|
}
|
|
3208
|
+
function removeSeenStore(path) {
|
|
3209
|
+
try {
|
|
3210
|
+
rmSync(path, { force: true, recursive: true });
|
|
3211
|
+
} catch {
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
var LOCK_WAIT_MS = 2e3;
|
|
3215
|
+
var LOCK_STALE_MS = 1e4;
|
|
3216
|
+
var pause2 = new Int32Array(new SharedArrayBuffer(4));
|
|
3217
|
+
function lockIsStale(lock) {
|
|
3218
|
+
try {
|
|
3219
|
+
return Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS;
|
|
3220
|
+
} catch {
|
|
3221
|
+
return false;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
function takeLock(lock) {
|
|
3225
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
3226
|
+
for (; ; ) {
|
|
3227
|
+
try {
|
|
3228
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
3229
|
+
mkdirSync(lock);
|
|
3230
|
+
return true;
|
|
3231
|
+
} catch (err) {
|
|
3232
|
+
if (err.code !== "EEXIST" && !isTransientFsError(err)) {
|
|
3233
|
+
return false;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
if (lockIsStale(lock)) removeSeenStore(lock);
|
|
3237
|
+
else if (Date.now() >= deadline) return false;
|
|
3238
|
+
else Atomics.wait(pause2, 0, 0, 5);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
function updateSeenStore(path, update) {
|
|
3242
|
+
const lock = `${path}.lock`;
|
|
3243
|
+
const locked = takeLock(lock);
|
|
3244
|
+
try {
|
|
3245
|
+
const next = update(readSeenStore(path));
|
|
3246
|
+
if (next !== null) writeSeenStore(path, next.data);
|
|
3247
|
+
} finally {
|
|
3248
|
+
if (locked) removeSeenStore(lock);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
1980
3251
|
|
|
1981
3252
|
// src/lessons/keyword-match.ts
|
|
1982
3253
|
function deriveHaystackTokens(query) {
|
|
@@ -1984,13 +3255,13 @@ function deriveHaystackTokens(query) {
|
|
|
1984
3255
|
if (query.file !== void 0) parts.push(query.file);
|
|
1985
3256
|
if (query.command !== void 0) parts.push(query.command);
|
|
1986
3257
|
if (parts.length === 0) return [];
|
|
1987
|
-
const
|
|
3258
|
+
const out2 = [];
|
|
1988
3259
|
for (const raw of splitTokens(parts.join(" "))) {
|
|
1989
|
-
|
|
3260
|
+
out2.push(raw.toLowerCase());
|
|
1990
3261
|
const sub = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(" ").filter((t) => t.length > 0);
|
|
1991
|
-
if (sub.length > 1)
|
|
3262
|
+
if (sub.length > 1) out2.push(...sub);
|
|
1992
3263
|
}
|
|
1993
|
-
return
|
|
3264
|
+
return out2;
|
|
1994
3265
|
}
|
|
1995
3266
|
function splitTokens(text) {
|
|
1996
3267
|
return text.split(/[^A-Za-z0-9]+/).filter((t) => t.length > 0);
|
|
@@ -2010,7 +3281,7 @@ function containsRun(needle, hay) {
|
|
|
2010
3281
|
return false;
|
|
2011
3282
|
}
|
|
2012
3283
|
function keywordMatches(pattern, query) {
|
|
2013
|
-
const needle =
|
|
3284
|
+
const needle = tokenize2(pattern);
|
|
2014
3285
|
if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
|
|
2015
3286
|
return true;
|
|
2016
3287
|
}
|
|
@@ -2019,6 +3290,7 @@ function keywordMatches(pattern, query) {
|
|
|
2019
3290
|
|
|
2020
3291
|
// src/lessons/query.ts
|
|
2021
3292
|
var COMMAND_MATCH_BUDGET = 5e6;
|
|
3293
|
+
var GLOB_MATCH_BUDGET = 2e6;
|
|
2022
3294
|
function queryLessons(graph, query) {
|
|
2023
3295
|
if (query.file === void 0 && query.command === void 0 && query.keyword === void 0) {
|
|
2024
3296
|
return [];
|
|
@@ -2041,9 +3313,12 @@ function collectMatchedTriggersByKind(graph, query) {
|
|
|
2041
3313
|
command_pattern: /* @__PURE__ */ new Set(),
|
|
2042
3314
|
keyword: /* @__PURE__ */ new Set()
|
|
2043
3315
|
};
|
|
2044
|
-
const
|
|
3316
|
+
const budgets = {
|
|
3317
|
+
command: { remaining: COMMAND_MATCH_BUDGET },
|
|
3318
|
+
glob: { remaining: GLOB_MATCH_BUDGET }
|
|
3319
|
+
};
|
|
2045
3320
|
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
2046
|
-
if (triggerMatches(trigger, query,
|
|
3321
|
+
if (triggerMatches(trigger, query, budgets)) byKind[trigger.kind].add(id);
|
|
2047
3322
|
}
|
|
2048
3323
|
return byKind;
|
|
2049
3324
|
}
|
|
@@ -2051,47 +3326,121 @@ function collectMatchedTriggerIds(graph, query) {
|
|
|
2051
3326
|
const { file_glob, command_pattern, keyword } = collectMatchedTriggersByKind(graph, query);
|
|
2052
3327
|
return /* @__PURE__ */ new Set([...file_glob, ...command_pattern, ...keyword]);
|
|
2053
3328
|
}
|
|
2054
|
-
function triggerMatches(trigger, query,
|
|
3329
|
+
function triggerMatches(trigger, query, budgets) {
|
|
2055
3330
|
switch (trigger.kind) {
|
|
2056
|
-
case "file_glob":
|
|
3331
|
+
case "file_glob": {
|
|
2057
3332
|
if (query.file === void 0) return false;
|
|
2058
|
-
|
|
3333
|
+
const matcher = getGlobMatcher(trigger.pattern);
|
|
3334
|
+
return matcher !== null && matcher.test(query.file, budgets.glob);
|
|
3335
|
+
}
|
|
2059
3336
|
case "command_pattern": {
|
|
2060
3337
|
if (query.command === void 0) return false;
|
|
2061
3338
|
const matcher = getCommandMatcher(trigger.pattern);
|
|
2062
|
-
return matcher !== null && matcher.test(query.command,
|
|
3339
|
+
return matcher !== null && matcher.test(query.command, budgets.command);
|
|
2063
3340
|
}
|
|
2064
3341
|
case "keyword":
|
|
2065
3342
|
return keywordMatches(trigger.pattern, query);
|
|
2066
3343
|
}
|
|
2067
3344
|
}
|
|
3345
|
+
function commandCouldMatch(commandPatterns, keywordPatterns, command) {
|
|
3346
|
+
const budget = { remaining: COMMAND_MATCH_BUDGET };
|
|
3347
|
+
const query = { command };
|
|
3348
|
+
return commandPatterns.some((p) => {
|
|
3349
|
+
const matcher = getCommandMatcher(p);
|
|
3350
|
+
return matcher !== null && matcher.test(command, budget);
|
|
3351
|
+
}) || keywordPatterns.some((p) => keywordMatches(p, query));
|
|
3352
|
+
}
|
|
2068
3353
|
function appendJsonl(path, record, opts) {
|
|
2069
|
-
|
|
2070
|
-
|
|
3354
|
+
try {
|
|
3355
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3356
|
+
const lead = endsWithoutNewline(path) ? "\n" : "";
|
|
3357
|
+
appendFileSync(path, `${lead}${JSON.stringify(record)}
|
|
2071
3358
|
`, "utf8");
|
|
2072
|
-
|
|
3359
|
+
if (statSync(path).size > opts.trimTriggerBytes) {
|
|
3360
|
+
capJsonl(path, opts.maxRecords, opts.trimTriggerBytes / 2);
|
|
3361
|
+
}
|
|
3362
|
+
} catch {
|
|
3363
|
+
}
|
|
2073
3364
|
}
|
|
2074
|
-
function
|
|
3365
|
+
function endsWithoutNewline(path) {
|
|
3366
|
+
let fd;
|
|
3367
|
+
try {
|
|
3368
|
+
const size = statSync(path).size;
|
|
3369
|
+
if (size === 0) return false;
|
|
3370
|
+
fd = openSync(path, "r");
|
|
3371
|
+
const last = Buffer.alloc(1);
|
|
3372
|
+
readSync(fd, last, 0, 1, size - 1);
|
|
3373
|
+
return last[0] !== 10;
|
|
3374
|
+
} catch {
|
|
3375
|
+
return false;
|
|
3376
|
+
} finally {
|
|
3377
|
+
if (fd !== void 0) closeSync(fd);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function capJsonl(path, maxRecords, maxBytes = Infinity) {
|
|
2075
3381
|
if (!existsSync(path)) return;
|
|
2076
3382
|
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
2077
|
-
|
|
2078
|
-
|
|
3383
|
+
const kept = [];
|
|
3384
|
+
let bytes = 0;
|
|
3385
|
+
for (let i = lines.length - 1; i >= 0 && kept.length < maxRecords; i -= 1) {
|
|
3386
|
+
const size = Buffer.byteLength(lines[i]) + 1;
|
|
3387
|
+
if (size > maxBytes) continue;
|
|
3388
|
+
if (bytes + size > maxBytes) break;
|
|
3389
|
+
bytes += size;
|
|
3390
|
+
kept.push(lines[i]);
|
|
3391
|
+
}
|
|
3392
|
+
if (kept.length === lines.length) return;
|
|
2079
3393
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
2080
|
-
writeFileSync(tmp, `${kept.join("\n")}
|
|
3394
|
+
writeFileSync(tmp, kept.length === 0 ? "" : `${kept.reverse().join("\n")}
|
|
2081
3395
|
`, "utf8");
|
|
2082
3396
|
renameSync(tmp, path);
|
|
2083
3397
|
}
|
|
2084
|
-
function
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
3398
|
+
function readTail(path, maxBytes) {
|
|
3399
|
+
const size = maxBytes === void 0 ? 0 : statSync(path).size;
|
|
3400
|
+
if (maxBytes === void 0 || size <= maxBytes) return readFileSync(path, "utf8");
|
|
3401
|
+
const fd = openSync(path, "r");
|
|
3402
|
+
try {
|
|
3403
|
+
const tail = Buffer.alloc(maxBytes);
|
|
3404
|
+
readSync(fd, tail, 0, maxBytes, size - maxBytes);
|
|
3405
|
+
const text = tail.toString("utf8");
|
|
3406
|
+
return text.slice(text.indexOf("\n") + 1);
|
|
3407
|
+
} finally {
|
|
3408
|
+
closeSync(fd);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function readJsonl(path, isRecord2, opts = {}) {
|
|
3412
|
+
let text;
|
|
3413
|
+
try {
|
|
3414
|
+
text = readTail(path, opts.maxBytes);
|
|
3415
|
+
} catch {
|
|
3416
|
+
return [];
|
|
3417
|
+
}
|
|
3418
|
+
const out2 = [];
|
|
3419
|
+
for (const line of text.split("\n")) {
|
|
2088
3420
|
if (line.trim().length === 0) continue;
|
|
2089
3421
|
try {
|
|
2090
|
-
|
|
3422
|
+
const value = JSON.parse(line);
|
|
3423
|
+
if (isRecord2(value)) out2.push(value);
|
|
2091
3424
|
} catch {
|
|
2092
3425
|
}
|
|
2093
3426
|
}
|
|
2094
|
-
return
|
|
3427
|
+
return out2;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// src/utils/types/guards.ts
|
|
3431
|
+
function isRecord(value) {
|
|
3432
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// src/lessons/log-record-guards.ts
|
|
3436
|
+
var isStr = (v) => typeof v === "string";
|
|
3437
|
+
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
3438
|
+
var optional = (v, check) => v === void 0 || check(v);
|
|
3439
|
+
function isOutcomeEvent(v) {
|
|
3440
|
+
if (!isRecord(v) || !isStr(v.ts) || !isStr(v.contextKey)) return false;
|
|
3441
|
+
if (!optional(v.session, isStr)) return false;
|
|
3442
|
+
if (v.kind === "delivered") return isStr(v.lessonId) && optional(v.rank, isNum);
|
|
3443
|
+
return v.kind === "failure" && optional(v.errorClass, isStr);
|
|
2095
3444
|
}
|
|
2096
3445
|
|
|
2097
3446
|
// src/lessons/telemetry.ts
|
|
@@ -2106,21 +3455,26 @@ function sessionId(env = process.env) {
|
|
|
2106
3455
|
function recallLogPath(projectRoot) {
|
|
2107
3456
|
return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
|
|
2108
3457
|
}
|
|
2109
|
-
function
|
|
3458
|
+
function configFlag(projectRoot, key) {
|
|
2110
3459
|
const path = lessonsPaths(projectRoot).config;
|
|
2111
|
-
if (!existsSync(path)) return
|
|
3460
|
+
if (!existsSync(path)) return void 0;
|
|
2112
3461
|
try {
|
|
2113
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2114
|
-
|
|
3462
|
+
const parsed = JSON.parse(stripBom(readFileSync(path, "utf8")));
|
|
3463
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
3464
|
+
const value = parsed[key];
|
|
3465
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2115
3466
|
} catch {
|
|
2116
|
-
return
|
|
3467
|
+
return void 0;
|
|
2117
3468
|
}
|
|
2118
3469
|
}
|
|
3470
|
+
function envOverride(raw) {
|
|
3471
|
+
const value = raw?.trim().toLowerCase();
|
|
3472
|
+
if (value === "1" || value === "true" || value === "yes" || value === "on") return true;
|
|
3473
|
+
if (value === "0" || value === "false" || value === "no" || value === "off") return false;
|
|
3474
|
+
return void 0;
|
|
3475
|
+
}
|
|
2119
3476
|
function isTelemetryEnabled(env = process.env, projectRoot) {
|
|
2120
|
-
|
|
2121
|
-
if (raw === "1") return true;
|
|
2122
|
-
if (raw === "0") return false;
|
|
2123
|
-
return projectRoot !== void 0 && configTelemetry(projectRoot);
|
|
3477
|
+
return envOverride(env[TELEMETRY_ENV]) ?? (projectRoot !== void 0 && configFlag(projectRoot, "telemetry") === true);
|
|
2124
3478
|
}
|
|
2125
3479
|
function appendRecallRecord(projectRoot, record, env = process.env) {
|
|
2126
3480
|
if (!isTelemetryEnabled(env, projectRoot)) return;
|
|
@@ -2150,15 +3504,15 @@ function readCache(path) {
|
|
|
2150
3504
|
try {
|
|
2151
3505
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2152
3506
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2153
|
-
const
|
|
2154
|
-
const stamp =
|
|
2155
|
-
if (typeof stamp?.mtimeMs !== "number" || typeof stamp.size !== "number" || !isStringArray(
|
|
3507
|
+
const c2 = parsed;
|
|
3508
|
+
const stamp = c2.stamp;
|
|
3509
|
+
if (typeof stamp?.mtimeMs !== "number" || typeof stamp.size !== "number" || !isStringArray(c2.commandPatterns) || !isStringArray(c2.keywordPatterns)) {
|
|
2156
3510
|
return null;
|
|
2157
3511
|
}
|
|
2158
3512
|
return {
|
|
2159
3513
|
stamp: { mtimeMs: stamp.mtimeMs, size: stamp.size },
|
|
2160
|
-
commandPatterns:
|
|
2161
|
-
keywordPatterns:
|
|
3514
|
+
commandPatterns: c2.commandPatterns,
|
|
3515
|
+
keywordPatterns: c2.keywordPatterns
|
|
2162
3516
|
};
|
|
2163
3517
|
} catch {
|
|
2164
3518
|
return null;
|
|
@@ -2187,34 +3541,14 @@ function refreshCommandFastpath(projectRoot, graph, preReadStamp) {
|
|
|
2187
3541
|
if (trigger.kind === "command_pattern") commandPatterns.push(trigger.pattern);
|
|
2188
3542
|
else if (trigger.kind === "keyword") keywordPatterns.push(trigger.pattern);
|
|
2189
3543
|
}
|
|
2190
|
-
const
|
|
3544
|
+
const cache4 = { stamp, commandPatterns, keywordPatterns };
|
|
2191
3545
|
mkdirSync(dirname(path), { recursive: true });
|
|
2192
3546
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
2193
|
-
writeFileSync(tmp, JSON.stringify(
|
|
3547
|
+
writeFileSync(tmp, JSON.stringify(cache4), "utf8");
|
|
2194
3548
|
renameSync(tmp, path);
|
|
2195
3549
|
} catch {
|
|
2196
3550
|
}
|
|
2197
3551
|
}
|
|
2198
|
-
function normalizeRecallFile(file, projectRoot) {
|
|
2199
|
-
const forward = file.replaceAll("\\", "/");
|
|
2200
|
-
const direct = relativize(projectRoot, forward);
|
|
2201
|
-
if (!direct.startsWith("../")) return direct;
|
|
2202
|
-
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
2203
|
-
return viaReal.startsWith("../") ? direct : viaReal;
|
|
2204
|
-
}
|
|
2205
|
-
function relativize(root, forward) {
|
|
2206
|
-
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
2207
|
-
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
2208
|
-
}
|
|
2209
|
-
function safeRealpath(path) {
|
|
2210
|
-
try {
|
|
2211
|
-
return realpathSync(path);
|
|
2212
|
-
} catch {
|
|
2213
|
-
const parent = dirname(path);
|
|
2214
|
-
if (parent === path) return path;
|
|
2215
|
-
return resolve(safeRealpath(parent), basename(path));
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
3552
|
|
|
2219
3553
|
// src/lessons/lexical-retrieval.ts
|
|
2220
3554
|
var LEXICAL_LIMIT = 3;
|
|
@@ -2235,14 +3569,14 @@ function matchLessons(graph, query) {
|
|
|
2235
3569
|
return { matches: [...triggered, ...lexical], lexicalCount: lexical.length };
|
|
2236
3570
|
}
|
|
2237
3571
|
function lexicalCandidates(graph, keyword, excludeIds) {
|
|
2238
|
-
const terms = [...new Set(
|
|
3572
|
+
const terms = [...new Set(tokenize2(keyword))];
|
|
2239
3573
|
if (terms.length < LEXICAL_MIN_TERMS) return [];
|
|
2240
3574
|
const excluded = new Set(excludeIds);
|
|
2241
3575
|
const corpus = buildCorpus(graph);
|
|
2242
3576
|
const scored = [];
|
|
2243
3577
|
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2244
3578
|
if (lesson.status !== "active" || lesson.scope === "always" || excluded.has(id)) continue;
|
|
2245
|
-
const ruleTerms = new Set(
|
|
3579
|
+
const ruleTerms = new Set(tokenize2(lesson.rule));
|
|
2246
3580
|
let shared = 0;
|
|
2247
3581
|
for (const t of terms) if (!GENERIC.has(t) && ruleTerms.has(t)) shared += 1;
|
|
2248
3582
|
if (shared < LEXICAL_MIN_TERMS) continue;
|
|
@@ -2363,32 +3697,39 @@ function rankLessons(graph, query, matches, options = {}) {
|
|
|
2363
3697
|
return applyCaps(ranked, options);
|
|
2364
3698
|
}
|
|
2365
3699
|
function applyCaps(ranked, options) {
|
|
2366
|
-
let
|
|
2367
|
-
if (options.limit !== void 0 && options.limit >= 0)
|
|
2368
|
-
if (options.maxTokens !== void 0 &&
|
|
2369
|
-
const budgeted = [
|
|
2370
|
-
let used = estTokens(
|
|
2371
|
-
for (const row of
|
|
3700
|
+
let out2 = ranked;
|
|
3701
|
+
if (options.limit !== void 0 && options.limit >= 0) out2 = out2.slice(0, options.limit);
|
|
3702
|
+
if (options.maxTokens !== void 0 && out2.length > 0) {
|
|
3703
|
+
const budgeted = [out2[0]];
|
|
3704
|
+
let used = estTokens(out2[0].lesson.rule);
|
|
3705
|
+
for (const row of out2.slice(1)) {
|
|
2372
3706
|
const cost = estTokens(row.lesson.rule);
|
|
2373
3707
|
if (used + cost > options.maxTokens) break;
|
|
2374
3708
|
used += cost;
|
|
2375
3709
|
budgeted.push(row);
|
|
2376
3710
|
}
|
|
2377
|
-
|
|
3711
|
+
out2 = budgeted;
|
|
2378
3712
|
}
|
|
2379
|
-
return
|
|
3713
|
+
return out2;
|
|
2380
3714
|
}
|
|
2381
3715
|
function defaultLessonsConfig() {
|
|
2382
3716
|
return {
|
|
2383
3717
|
recallLimit: DEFAULT_RECALL_LIMIT,
|
|
2384
3718
|
recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
|
|
2385
3719
|
autoPrune: false,
|
|
2386
|
-
telemetry: false
|
|
3720
|
+
telemetry: false,
|
|
3721
|
+
outcomeLog: true
|
|
2387
3722
|
};
|
|
2388
3723
|
}
|
|
3724
|
+
var MAX_RECALL_LIMIT = 50;
|
|
3725
|
+
var MAX_RECALL_MAX_TOKENS = 8e3;
|
|
2389
3726
|
function positiveInt(value) {
|
|
2390
3727
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
2391
3728
|
}
|
|
3729
|
+
function clamped(value, ceiling) {
|
|
3730
|
+
const n = positiveInt(value);
|
|
3731
|
+
return n === null ? null : Math.min(n, ceiling);
|
|
3732
|
+
}
|
|
2392
3733
|
function loadRecallConfig(projectRoot) {
|
|
2393
3734
|
const fallback = {
|
|
2394
3735
|
limit: DEFAULT_RECALL_LIMIT,
|
|
@@ -2397,32 +3738,126 @@ function loadRecallConfig(projectRoot) {
|
|
|
2397
3738
|
const path = lessonsPaths(projectRoot).config;
|
|
2398
3739
|
if (!existsSync(path)) return fallback;
|
|
2399
3740
|
try {
|
|
2400
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
3741
|
+
const parsed = JSON.parse(stripBom(readFileSync(path, "utf8")));
|
|
2401
3742
|
if (typeof parsed !== "object" || parsed === null) return fallback;
|
|
2402
3743
|
const cfg = parsed;
|
|
2403
3744
|
return {
|
|
2404
|
-
limit:
|
|
2405
|
-
maxTokens:
|
|
3745
|
+
limit: clamped(cfg.recallLimit, MAX_RECALL_LIMIT) ?? fallback.limit,
|
|
3746
|
+
maxTokens: clamped(cfg.recallMaxTokens, MAX_RECALL_MAX_TOKENS) ?? fallback.maxTokens
|
|
2406
3747
|
};
|
|
2407
3748
|
} catch {
|
|
2408
3749
|
return fallback;
|
|
2409
3750
|
}
|
|
2410
3751
|
}
|
|
3752
|
+
var GLOBAL_VALUE_FLAGS = /* @__PURE__ */ new Map([
|
|
3753
|
+
[
|
|
3754
|
+
"git",
|
|
3755
|
+
["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--super-prefix", "--config-env"]
|
|
3756
|
+
],
|
|
3757
|
+
["npm", ["--prefix", "-w", "--workspace", "--userconfig", "--cache", "--loglevel"]],
|
|
3758
|
+
["pnpm", ["-C", "--dir", "-F", "--filter", "--loglevel", "--reporter"]],
|
|
3759
|
+
["yarn", ["--cwd"]],
|
|
3760
|
+
["npx", ["-p", "--package", "-c", "--call"]],
|
|
3761
|
+
["bun", ["--cwd", "--config"]],
|
|
3762
|
+
["bunx", ["-p", "--package"]],
|
|
3763
|
+
["make", ["-C", "--directory", "-f", "--file", "--makefile", "-I", "--include-dir"]],
|
|
3764
|
+
["docker", ["-H", "--host", "--context", "-c", "--config", "-l", "--log-level"]],
|
|
3765
|
+
[
|
|
3766
|
+
"kubectl",
|
|
3767
|
+
["-n", "--namespace", "--context", "--kubeconfig", "--cluster", "--user", "-s", "--server"]
|
|
3768
|
+
],
|
|
3769
|
+
["cargo", ["-C", "--config", "-Z", "--color"]],
|
|
3770
|
+
["go", ["-C"]]
|
|
3771
|
+
]);
|
|
3772
|
+
var SHELL_SETUP = /* @__PURE__ */ new Set([
|
|
3773
|
+
"cd",
|
|
3774
|
+
"pushd",
|
|
3775
|
+
"popd",
|
|
3776
|
+
"export",
|
|
3777
|
+
"set",
|
|
3778
|
+
"unset",
|
|
3779
|
+
"source",
|
|
3780
|
+
".",
|
|
3781
|
+
"[",
|
|
3782
|
+
"[[",
|
|
3783
|
+
"test",
|
|
3784
|
+
"true",
|
|
3785
|
+
"false",
|
|
3786
|
+
":"
|
|
3787
|
+
]);
|
|
3788
|
+
var SHELL_PREFIX = /* @__PURE__ */ new Set(["if", "then", "else", "elif", "do", "while", "until", "!", "time"]);
|
|
3789
|
+
var BLOCK_SYNTAX = /* @__PURE__ */ new Set(["for", "case", "select", "function", "done", "fi", "esac"]);
|
|
3790
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
3791
|
+
var SUBCOMMAND = /^[A-Za-z][A-Za-z0-9_-]*(?::[A-Za-z0-9_-]+)*$/;
|
|
3792
|
+
function splitSegments(command) {
|
|
3793
|
+
const out2 = [];
|
|
3794
|
+
let cur = "";
|
|
3795
|
+
let quote = null;
|
|
3796
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
3797
|
+
const ch = command[i];
|
|
3798
|
+
const next = command[i + 1];
|
|
3799
|
+
if (quote !== null) {
|
|
3800
|
+
cur += ch;
|
|
3801
|
+
if (ch === quote) quote = null;
|
|
3802
|
+
continue;
|
|
3803
|
+
}
|
|
3804
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
3805
|
+
quote = ch;
|
|
3806
|
+
cur += ch;
|
|
3807
|
+
} else if (ch === "\\" && next !== void 0) {
|
|
3808
|
+
cur += ch + next;
|
|
3809
|
+
i += 1;
|
|
3810
|
+
} else if (ch === ";" || ch === "\n" || ch === "|" || ch === "&" && next === "&") {
|
|
3811
|
+
out2.push(cur);
|
|
3812
|
+
cur = "";
|
|
3813
|
+
if (ch !== ";" && ch !== "\n" && (next === ch || next === "&")) i += 1;
|
|
3814
|
+
} else {
|
|
3815
|
+
cur += ch;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
out2.push(cur);
|
|
3819
|
+
return out2;
|
|
3820
|
+
}
|
|
3821
|
+
function programName(word) {
|
|
3822
|
+
return posix.basename(word.replaceAll("\\", "/")) || word;
|
|
3823
|
+
}
|
|
3824
|
+
function subcommandOf(program, args) {
|
|
3825
|
+
const valueFlags = GLOBAL_VALUE_FLAGS.get(program);
|
|
3826
|
+
let i = 0;
|
|
3827
|
+
while (valueFlags !== void 0 && i < args.length && /^-./.test(args[i])) {
|
|
3828
|
+
const flag = args[i];
|
|
3829
|
+
i += flag !== "--" && !flag.includes("=") && valueFlags.includes(flag) ? 2 : 1;
|
|
3830
|
+
if (flag === "--") break;
|
|
3831
|
+
}
|
|
3832
|
+
const next = args[i];
|
|
3833
|
+
return next !== void 0 && SUBCOMMAND.test(next) ? { subcommand: next, gapped: i > 0 } : { gapped: false };
|
|
3834
|
+
}
|
|
3835
|
+
function segmentClass(segment) {
|
|
3836
|
+
const words = segment.replace(/^[\s({]+/, "").replace(/[\s)}]+$/, "").split(/\s+/).filter((w) => w.length > 0);
|
|
3837
|
+
while (words.length > 0 && SHELL_PREFIX.has(words[0])) words.shift();
|
|
3838
|
+
const first = words[0];
|
|
3839
|
+
if (first === void 0 || first.startsWith("#") || BLOCK_SYNTAX.has(first)) return null;
|
|
3840
|
+
while (words.length > 0 && ASSIGNMENT.test(words[0])) words.shift();
|
|
3841
|
+
if (words.length === 0) return null;
|
|
3842
|
+
const program = programName(words[0]);
|
|
3843
|
+
return { program, ...subcommandOf(program, words.slice(1)) };
|
|
3844
|
+
}
|
|
3845
|
+
function commandClass(command) {
|
|
3846
|
+
let setup = null;
|
|
3847
|
+
for (const segment of splitSegments(command.replace(/\\\r?\n/g, " "))) {
|
|
3848
|
+
const cls = segmentClass(segment);
|
|
3849
|
+
if (cls === null) continue;
|
|
3850
|
+
if (!SHELL_SETUP.has(cls.program)) return cls;
|
|
3851
|
+
setup ??= { program: cls.program, gapped: false };
|
|
3852
|
+
}
|
|
3853
|
+
return setup;
|
|
3854
|
+
}
|
|
2411
3855
|
|
|
2412
3856
|
// src/lessons/context-key.ts
|
|
2413
3857
|
function normalizeCommand(command) {
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
const rest = words.slice(start);
|
|
2418
|
-
const programIdx = rest.findIndex(isBareWord);
|
|
2419
|
-
if (programIdx === -1) return rest[0] ?? "";
|
|
2420
|
-
const program = rest[programIdx];
|
|
2421
|
-
const next = rest[programIdx + 1];
|
|
2422
|
-
return next !== void 0 && isBareWord(next) ? `${program} ${next}` : program;
|
|
2423
|
-
}
|
|
2424
|
-
function isBareWord(w) {
|
|
2425
|
-
return w.length > 0 && !w.startsWith("-") && !w.includes("/") && !/^["'`]/.test(w);
|
|
3858
|
+
const cls = commandClass(command);
|
|
3859
|
+
if (cls === null) return "";
|
|
3860
|
+
return cls.subcommand === void 0 ? cls.program : `${cls.program} ${cls.subcommand}`;
|
|
2426
3861
|
}
|
|
2427
3862
|
function contextKey(input, projectRoot) {
|
|
2428
3863
|
if (input.file !== void 0 && input.file.length > 0) {
|
|
@@ -2461,40 +3896,114 @@ function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, opti
|
|
|
2461
3896
|
...session !== void 0 ? { session } : {}
|
|
2462
3897
|
});
|
|
2463
3898
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
3899
|
+
|
|
3900
|
+
// src/lessons/action-match.ts
|
|
3901
|
+
function queryFromContextKey(key) {
|
|
3902
|
+
if (key.startsWith("file:")) return { file: key.slice("file:".length) };
|
|
3903
|
+
if (key.startsWith("cmd:") && key.length > "cmd:".length)
|
|
3904
|
+
return { command: key.slice("cmd:".length) };
|
|
3905
|
+
return null;
|
|
2466
3906
|
}
|
|
2467
|
-
function
|
|
2468
|
-
|
|
3907
|
+
function createActionMatcher(graph) {
|
|
3908
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3909
|
+
return (lessonId, contextKey2) => {
|
|
3910
|
+
let byKey = seen.get(lessonId);
|
|
3911
|
+
if (byKey === void 0) {
|
|
3912
|
+
byKey = /* @__PURE__ */ new Map();
|
|
3913
|
+
seen.set(lessonId, byKey);
|
|
3914
|
+
}
|
|
3915
|
+
let hit = byKey.get(contextKey2);
|
|
3916
|
+
if (hit === void 0) {
|
|
3917
|
+
hit = matchesOwnTrigger(graph, lessonId, contextKey2);
|
|
3918
|
+
byKey.set(contextKey2, hit);
|
|
3919
|
+
}
|
|
3920
|
+
return hit;
|
|
3921
|
+
};
|
|
2469
3922
|
}
|
|
2470
|
-
function
|
|
2471
|
-
|
|
3923
|
+
function matchesOwnTrigger(graph, lessonId, contextKey2) {
|
|
3924
|
+
const query = queryFromContextKey(contextKey2);
|
|
3925
|
+
const lesson = graph.lessons[lessonId];
|
|
3926
|
+
if (query === null || lesson === void 0) return false;
|
|
3927
|
+
const triggers = lesson.triggers.map((id) => graph.triggers[id]).filter((t) => t !== void 0);
|
|
3928
|
+
const patterns = (kind) => triggers.filter((t) => t.kind === kind).map((t) => t.pattern);
|
|
3929
|
+
if ("command" in query) {
|
|
3930
|
+
return commandCouldMatch(patterns("command_pattern"), patterns("keyword"), query.command);
|
|
3931
|
+
}
|
|
3932
|
+
return patterns("file_glob").some((p) => getGlobMatcher(p)?.test(query.file) ?? false) || patterns("keyword").some((p) => keywordMatches(p, query));
|
|
2472
3933
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
3934
|
+
|
|
3935
|
+
// src/lessons/effectiveness.ts
|
|
3936
|
+
var INEFFECTIVE_MIN_DELIVERIES = 3;
|
|
3937
|
+
var MISS_WINDOW_MS = 30 * 60 * 1e3;
|
|
3938
|
+
function failuresBySession(events) {
|
|
3939
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
3940
|
+
events.forEach((ev, index) => {
|
|
3941
|
+
const at = Date.parse(ev.ts);
|
|
3942
|
+
if (ev.kind !== "failure" || ev.session === void 0 || !Number.isFinite(at)) return;
|
|
3943
|
+
const list = out2.get(ev.session) ?? [];
|
|
3944
|
+
list.push({ index, at, contextKey: ev.contextKey });
|
|
3945
|
+
out2.set(ev.session, list);
|
|
2477
3946
|
});
|
|
2478
|
-
|
|
2479
|
-
|
|
3947
|
+
return out2;
|
|
3948
|
+
}
|
|
3949
|
+
function impeachingFailure(ev, index, failures, matches) {
|
|
3950
|
+
const at = Date.parse(ev.ts);
|
|
3951
|
+
if (ev.session === void 0 || !Number.isFinite(at)) return void 0;
|
|
3952
|
+
return failures.get(ev.session)?.find(
|
|
3953
|
+
(f) => f.index > index && f.at >= at && f.at - at <= MISS_WINDOW_MS && matches(ev.lessonId, f.contextKey)
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
function effectiveness(events, graph) {
|
|
3957
|
+
const failures = failuresBySession(events);
|
|
3958
|
+
const matches = createActionMatcher(graph);
|
|
3959
|
+
const acc = /* @__PURE__ */ new Map();
|
|
3960
|
+
events.forEach((ev, index) => {
|
|
2480
3961
|
if (ev.kind !== "delivered") return;
|
|
2481
|
-
const cur =
|
|
3962
|
+
const cur = acc.get(ev.lessonId) ?? { delivered: 0, missed: 0, actions: /* @__PURE__ */ new Set() };
|
|
3963
|
+
acc.set(ev.lessonId, cur);
|
|
2482
3964
|
cur.delivered += 1;
|
|
2483
|
-
const
|
|
2484
|
-
if (
|
|
2485
|
-
|
|
3965
|
+
const hit = impeachingFailure(ev, index, failures, matches);
|
|
3966
|
+
if (hit === void 0) return;
|
|
3967
|
+
cur.missed += 1;
|
|
3968
|
+
cur.actions.add(hit.contextKey);
|
|
2486
3969
|
});
|
|
2487
|
-
|
|
3970
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
3971
|
+
for (const [id, a] of acc) {
|
|
3972
|
+
out2.set(id, {
|
|
3973
|
+
delivered: a.delivered,
|
|
3974
|
+
missed: a.missed,
|
|
3975
|
+
failingActions: [...a.actions].sort()
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
return out2;
|
|
2488
3979
|
}
|
|
2489
3980
|
function effectivenessScore(o) {
|
|
2490
3981
|
return o.delivered === 0 ? 1 : 1 - o.missed / o.delivered;
|
|
2491
3982
|
}
|
|
2492
|
-
function
|
|
2493
|
-
const
|
|
2494
|
-
for (const [id, o] of effectiveness(
|
|
2495
|
-
|
|
3983
|
+
function effectivenessScores(events, graph) {
|
|
3984
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
3985
|
+
for (const [id, o] of effectiveness(events, graph)) {
|
|
3986
|
+
if (o.delivered >= INEFFECTIVE_MIN_DELIVERIES) out2.set(id, effectivenessScore(o));
|
|
2496
3987
|
}
|
|
2497
|
-
return
|
|
3988
|
+
return out2;
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
// src/lessons/outcome-log.ts
|
|
3992
|
+
var OUTCOME_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
3993
|
+
function outcomeLogPath(projectRoot) {
|
|
3994
|
+
return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
|
|
3995
|
+
}
|
|
3996
|
+
function readOutcomeLog(projectRoot) {
|
|
3997
|
+
return readJsonl(outcomeLogPath(projectRoot), isOutcomeEvent, {
|
|
3998
|
+
maxBytes: OUTCOME_LOG_TRIM_TRIGGER_BYTES
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
function loadEffectiveness(projectRoot, graph, lessonIds) {
|
|
4002
|
+
const all = readOutcomeLog(projectRoot);
|
|
4003
|
+
const events = lessonIds === void 0 ? all : all.filter((e) => e.kind !== "delivered" || lessonIds.has(e.lessonId));
|
|
4004
|
+
const kinds = new Set(events.map((e) => e.kind));
|
|
4005
|
+
if (!kinds.has("delivered") || !kinds.has("failure")) return /* @__PURE__ */ new Map();
|
|
4006
|
+
return effectivenessScores(events, graph);
|
|
2498
4007
|
}
|
|
2499
4008
|
var AUTO_SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
2500
4009
|
var FUTURE_TOLERANCE_MS = 6e4;
|
|
@@ -2523,7 +4032,8 @@ function openSessionDedup(options = {}) {
|
|
|
2523
4032
|
seen: stale ? /* @__PURE__ */ new Set() : visibleSeen(store.ids, stamps, options.ttlMs),
|
|
2524
4033
|
path,
|
|
2525
4034
|
stamps,
|
|
2526
|
-
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
4035
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {},
|
|
4036
|
+
...stale ? { resetAt: Date.now() } : {}
|
|
2527
4037
|
};
|
|
2528
4038
|
}
|
|
2529
4039
|
function visibleSeen(ids, stamps, ttlMs) {
|
|
@@ -2538,27 +4048,26 @@ function filterUnseen(dedup, matches) {
|
|
|
2538
4048
|
return matches.filter((m) => !dedup.seen.has(m.id));
|
|
2539
4049
|
}
|
|
2540
4050
|
function commitSeen(dedup, returnedIds) {
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
4051
|
+
updateSeenStore(dedup.path, (latest) => {
|
|
4052
|
+
const store = dedup.resetAt !== void 0 && (latest.lastAt ?? 0) <= dedup.resetAt ? EMPTY : latest;
|
|
4053
|
+
if (returnedIds.length === 0) {
|
|
4054
|
+
return dedup.ttlMs !== void 0 && store.stamps !== null ? { data: store.stamps } : null;
|
|
2544
4055
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
4056
|
+
if (dedup.ttlMs !== void 0 || store.stamps !== null) {
|
|
4057
|
+
const now = Date.now();
|
|
4058
|
+
const merged = /* @__PURE__ */ new Map();
|
|
4059
|
+
for (const [id, ms] of store.stamps ?? []) {
|
|
4060
|
+
if (dedup.ttlMs === void 0 || now - ms <= dedup.ttlMs) merged.set(id, ms);
|
|
4061
|
+
}
|
|
4062
|
+
for (const id of returnedIds) merged.set(id, now);
|
|
4063
|
+
return { data: merged };
|
|
2552
4064
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
return;
|
|
2556
|
-
}
|
|
2557
|
-
const union2 = new Set(dedup.seen);
|
|
2558
|
-
for (const id of returnedIds) union2.add(id);
|
|
2559
|
-
if (union2.size === dedup.seen.size) return;
|
|
2560
|
-
writeSeenStore(dedup.path, [...union2]);
|
|
4065
|
+
const union2 = new Set(store.ids);
|
|
4066
|
+
for (const id of returnedIds) union2.add(id);
|
|
4067
|
+
return union2.size === store.ids.size ? null : { data: [...union2] };
|
|
4068
|
+
});
|
|
2561
4069
|
}
|
|
4070
|
+
var EMPTY = { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2562
4071
|
|
|
2563
4072
|
// src/lessons/recall.ts
|
|
2564
4073
|
async function recallLessons(projectRoot, query, options = {}) {
|
|
@@ -2594,7 +4103,7 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2594
4103
|
// unchanged until the outcome log has real signal). Read from the side-channel
|
|
2595
4104
|
// only when something survived matching+dedup — a no-match recall must not pay
|
|
2596
4105
|
// the (up to 2MB) outcome-log read for a ranking of nothing.
|
|
2597
|
-
effectiveness: forRank.length === 0 ? /* @__PURE__ */ new Map() : loadEffectiveness(projectRoot)
|
|
4106
|
+
effectiveness: forRank.length === 0 ? /* @__PURE__ */ new Map() : loadEffectiveness(projectRoot, graph, new Set(forRank.map((m) => m.id)))
|
|
2598
4107
|
});
|
|
2599
4108
|
if (dedup !== null)
|
|
2600
4109
|
commitSeen(
|
|
@@ -2634,7 +4143,7 @@ function planPrune(graph, options = {}) {
|
|
|
2634
4143
|
const removedDeadGlobs = [];
|
|
2635
4144
|
const unreachableLessons = [];
|
|
2636
4145
|
if (options.knownPaths !== void 0) {
|
|
2637
|
-
const dead =
|
|
4146
|
+
const { dead } = fileGlobLiveness(graph, options.knownPaths);
|
|
2638
4147
|
if (dead.size > 0) {
|
|
2639
4148
|
for (const [id, kept] of keptByLesson) {
|
|
2640
4149
|
const deadInLesson = kept.filter((t) => dead.has(t));
|
|
@@ -2658,7 +4167,14 @@ function planPrune(graph, options = {}) {
|
|
|
2658
4167
|
for (const topic of lesson.topics) referencedTopics.add(topic);
|
|
2659
4168
|
}
|
|
2660
4169
|
const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
|
|
2661
|
-
return {
|
|
4170
|
+
return {
|
|
4171
|
+
removedTriggerIds,
|
|
4172
|
+
removedTopicIds,
|
|
4173
|
+
trimmedLessons,
|
|
4174
|
+
removedDeadGlobs,
|
|
4175
|
+
unreachableLessons,
|
|
4176
|
+
cap
|
|
4177
|
+
};
|
|
2662
4178
|
}
|
|
2663
4179
|
function applyPruneToGraph(graph, plan) {
|
|
2664
4180
|
for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
|
|
@@ -2683,15 +4199,7 @@ function isEmptyPrunePlan(plan) {
|
|
|
2683
4199
|
|
|
2684
4200
|
// src/lessons/auto-prune.ts
|
|
2685
4201
|
function isAutoPruneEnabled(projectRoot) {
|
|
2686
|
-
|
|
2687
|
-
if (!existsSync(path)) return false;
|
|
2688
|
-
try {
|
|
2689
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2690
|
-
if (typeof parsed !== "object" || parsed === null) return false;
|
|
2691
|
-
return parsed.autoPrune === true;
|
|
2692
|
-
} catch {
|
|
2693
|
-
return false;
|
|
2694
|
-
}
|
|
4202
|
+
return configFlag(projectRoot, "autoPrune") === true;
|
|
2695
4203
|
}
|
|
2696
4204
|
async function maybeAutoPrune(projectRoot, knownPaths) {
|
|
2697
4205
|
if (!isAutoPruneEnabled(projectRoot)) return null;
|
|
@@ -2743,28 +4251,6 @@ function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
|
|
|
2743
4251
|
env
|
|
2744
4252
|
);
|
|
2745
4253
|
}
|
|
2746
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2747
|
-
var MAX_FILES = 2e5;
|
|
2748
|
-
function listProjectFiles(projectRoot) {
|
|
2749
|
-
const out = /* @__PURE__ */ new Set();
|
|
2750
|
-
try {
|
|
2751
|
-
const stack = [projectRoot];
|
|
2752
|
-
while (stack.length > 0) {
|
|
2753
|
-
const dir = stack.pop();
|
|
2754
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2755
|
-
if (entry.isDirectory()) {
|
|
2756
|
-
if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
2757
|
-
} else if (entry.isFile()) {
|
|
2758
|
-
out.add(toRelPath(projectRoot, join(dir, entry.name)));
|
|
2759
|
-
if (out.size > MAX_FILES) return out;
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
} catch {
|
|
2764
|
-
return null;
|
|
2765
|
-
}
|
|
2766
|
-
return out;
|
|
2767
|
-
}
|
|
2768
4254
|
|
|
2769
4255
|
// src/lessons/capture.ts
|
|
2770
4256
|
async function captureLesson(projectRoot, input, options = {}) {
|
|
@@ -2794,19 +4280,17 @@ async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
|
|
|
2794
4280
|
}
|
|
2795
4281
|
function mergeInto(graph, loserId, keeperId) {
|
|
2796
4282
|
if (loserId === keeperId) {
|
|
2797
|
-
throw new Error(`
|
|
4283
|
+
throw new Error(`Cannot merge lesson "${loserId}" into itself.`);
|
|
2798
4284
|
}
|
|
2799
4285
|
const loser = graph.lessons[loserId];
|
|
2800
|
-
if (loser === void 0) throw new Error(`
|
|
4286
|
+
if (loser === void 0) throw new Error(`Unknown lesson "${loserId}".`);
|
|
2801
4287
|
const keeper = graph.lessons[keeperId];
|
|
2802
|
-
if (keeper === void 0) throw new Error(`
|
|
4288
|
+
if (keeper === void 0) throw new Error(`Unknown lesson "${keeperId}".`);
|
|
2803
4289
|
if (keeper.status !== "active") {
|
|
2804
|
-
throw new Error(`
|
|
4290
|
+
throw new Error(`Keeper "${keeperId}" is not active (status: ${keeper.status}).`);
|
|
2805
4291
|
}
|
|
2806
4292
|
if (loser.status !== "active") {
|
|
2807
|
-
throw new Error(
|
|
2808
|
-
`mergeLessons: loser "${loserId}" is already ${loser.status}; nothing to merge.`
|
|
2809
|
-
);
|
|
4293
|
+
throw new Error(`Loser "${loserId}" is already ${loser.status}; nothing to merge.`);
|
|
2810
4294
|
}
|
|
2811
4295
|
graph.lessons[keeperId] = {
|
|
2812
4296
|
...keeper,
|
|
@@ -2830,10 +4314,10 @@ var LINE_REF_PATTERNS = [
|
|
|
2830
4314
|
];
|
|
2831
4315
|
var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
|
|
2832
4316
|
function stripLegacyMarkers(rule) {
|
|
2833
|
-
let
|
|
2834
|
-
for (const pattern of LINE_REF_PATTERNS)
|
|
2835
|
-
|
|
2836
|
-
return
|
|
4317
|
+
let out2 = rule;
|
|
4318
|
+
for (const pattern of LINE_REF_PATTERNS) out2 = out2.replace(pattern, "");
|
|
4319
|
+
out2 = out2.replace(ALSO_RELEVANT_PATTERN, " ");
|
|
4320
|
+
return out2.trim();
|
|
2837
4321
|
}
|
|
2838
4322
|
function applyStrip(graph) {
|
|
2839
4323
|
const changedIds = [];
|
|
@@ -2859,14 +4343,43 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
|
|
|
2859
4343
|
});
|
|
2860
4344
|
return { changedIds, changedCount: changedIds.length };
|
|
2861
4345
|
}
|
|
2862
|
-
var
|
|
2863
|
-
var
|
|
4346
|
+
var LOCAL_FIRST = "npx --no --offline agentsmesh";
|
|
4347
|
+
var BARE = "agentsmesh";
|
|
4348
|
+
var DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
4349
|
+
function agentsmeshInvocation(projectRoot) {
|
|
4350
|
+
return dependsOnAgentsmesh(projectRoot) ? LOCAL_FIRST : BARE;
|
|
4351
|
+
}
|
|
4352
|
+
function dependsOnAgentsmesh(projectRoot) {
|
|
4353
|
+
try {
|
|
4354
|
+
const manifest = JSON.parse(
|
|
4355
|
+
readFileSync(join(projectRoot, "package.json"), "utf8")
|
|
4356
|
+
);
|
|
4357
|
+
return DEPENDENCY_FIELDS.some((field) => manifest?.[field]?.agentsmesh !== void 0);
|
|
4358
|
+
} catch {
|
|
4359
|
+
return false;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
// src/lessons/recall-hook-scaffold.ts
|
|
4364
|
+
var RECALL_SUBCOMMAND = "lessons hook";
|
|
4365
|
+
var RECALL_HOOK_COMMAND = `agentsmesh ${RECALL_SUBCOMMAND}`;
|
|
4366
|
+
var RECALL_HOOK_TOOL_MATCHER = "Edit|Write|NotebookEdit|Bash|PowerShell";
|
|
4367
|
+
var MANAGED_COMMAND = new RegExp(`^(?:npx(?: --?[\\w-]+)* )?${RECALL_HOOK_COMMAND}$`);
|
|
4368
|
+
function recallHookCommand(projectRoot) {
|
|
4369
|
+
return `${agentsmeshInvocation(projectRoot)} ${RECALL_SUBCOMMAND}`;
|
|
4370
|
+
}
|
|
4371
|
+
function isManagedRecallCommand(command) {
|
|
4372
|
+
return typeof command === "string" && MANAGED_COMMAND.test(command.trim());
|
|
4373
|
+
}
|
|
4374
|
+
function isManaged(item) {
|
|
4375
|
+
return item instanceof YAMLMap && isManagedRecallCommand(item.get("command"));
|
|
4376
|
+
}
|
|
2864
4377
|
var RECALL_EVENTS = [
|
|
2865
4378
|
{ event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
|
|
2866
4379
|
{ event: "UserPromptSubmit", matcher: "*" },
|
|
2867
|
-
// Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT:
|
|
2868
|
-
//
|
|
2869
|
-
//
|
|
4380
|
+
// Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: targets with
|
|
4381
|
+
// no failure event drop it without warning (BEST_EFFORT_HOOK_EVENTS).
|
|
4382
|
+
// PostToolUse is success-only, so failures need this.
|
|
2870
4383
|
{ event: "PostToolUseFailure", matcher: "*" },
|
|
2871
4384
|
// Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
|
|
2872
4385
|
// BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
|
|
@@ -2876,32 +4389,39 @@ var RETIRED_EVENTS = ["PostToolUse"];
|
|
|
2876
4389
|
function removeEvent(doc, event) {
|
|
2877
4390
|
const existing = doc.get(event);
|
|
2878
4391
|
if (!(existing instanceof YAMLSeq)) return false;
|
|
2879
|
-
const kept = existing.items.filter(
|
|
2880
|
-
(item) => !(item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND)
|
|
2881
|
-
);
|
|
4392
|
+
const kept = existing.items.filter((item) => !isManaged(item));
|
|
2882
4393
|
if (kept.length === existing.items.length) return false;
|
|
2883
4394
|
if (kept.length === 0) doc.delete(event);
|
|
2884
4395
|
else existing.items = kept;
|
|
2885
4396
|
return true;
|
|
2886
4397
|
}
|
|
2887
|
-
function
|
|
4398
|
+
function upsertEvent(doc, event, matcher, command) {
|
|
2888
4399
|
const existing = doc.get(event);
|
|
2889
4400
|
const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
|
|
2890
|
-
const
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
4401
|
+
const [first, ...extra] = seq.items.filter(isManaged);
|
|
4402
|
+
if (first === void 0) {
|
|
4403
|
+
seq.add(doc.createNode({ matcher, type: "command", command }));
|
|
4404
|
+
doc.set(event, seq);
|
|
4405
|
+
return true;
|
|
4406
|
+
}
|
|
4407
|
+
let changed = extra.length > 0;
|
|
4408
|
+
if (changed) seq.items = seq.items.filter((item) => !extra.includes(item));
|
|
4409
|
+
const desired = { matcher, type: "command", command };
|
|
4410
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
4411
|
+
if (first.get(key) === value) continue;
|
|
4412
|
+
first.set(key, value);
|
|
4413
|
+
changed = true;
|
|
4414
|
+
}
|
|
4415
|
+
return changed;
|
|
2897
4416
|
}
|
|
2898
4417
|
function injectRecallHook(projectRoot) {
|
|
2899
4418
|
const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
|
|
2900
4419
|
if (!existsSync(path)) return false;
|
|
2901
4420
|
const doc = parseDocument(readFileSync(path, "utf8"));
|
|
4421
|
+
const command = recallHookCommand(projectRoot);
|
|
2902
4422
|
let changed = false;
|
|
2903
4423
|
for (const { event, matcher } of RECALL_EVENTS) {
|
|
2904
|
-
if (
|
|
4424
|
+
if (upsertEvent(doc, event, matcher, command)) changed = true;
|
|
2905
4425
|
}
|
|
2906
4426
|
for (const event of RETIRED_EVENTS) {
|
|
2907
4427
|
if (removeEvent(doc, event)) changed = true;
|
|
@@ -2909,165 +4429,117 @@ function injectRecallHook(projectRoot) {
|
|
|
2909
4429
|
if (changed) writeFileSync(path, String(doc), "utf8");
|
|
2910
4430
|
return changed;
|
|
2911
4431
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
var LESSONS_GITATTRIBUTES_ENTRY = `.agentsmesh/lessons/lessons.json merge=${LESSONS_MERGE_DRIVER}`;
|
|
2916
|
-
var UTF8_BOM = "\uFEFF";
|
|
2917
|
-
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2918
|
-
".md",
|
|
2919
|
-
".mdc",
|
|
2920
|
-
".mdx",
|
|
2921
|
-
".markdown",
|
|
2922
|
-
".txt",
|
|
2923
|
-
".json",
|
|
2924
|
-
".jsonc",
|
|
2925
|
-
".yaml",
|
|
2926
|
-
".yml",
|
|
2927
|
-
".toml",
|
|
2928
|
-
".ini",
|
|
2929
|
-
".sh",
|
|
2930
|
-
".bash",
|
|
2931
|
-
".zsh",
|
|
2932
|
-
".ps1",
|
|
2933
|
-
".js",
|
|
2934
|
-
".mjs",
|
|
2935
|
-
".cjs",
|
|
2936
|
-
".ts",
|
|
2937
|
-
".tsx",
|
|
2938
|
-
".html",
|
|
2939
|
-
".css"
|
|
2940
|
-
]);
|
|
2941
|
-
var TEXT_DOTFILES = /* @__PURE__ */ new Set([
|
|
2942
|
-
".gitignore",
|
|
2943
|
-
".cursorignore",
|
|
2944
|
-
".cursorindexingignore",
|
|
2945
|
-
".aiignore",
|
|
2946
|
-
".agentignore",
|
|
2947
|
-
".clineignore",
|
|
2948
|
-
".geminiignore",
|
|
2949
|
-
".codeiumignore",
|
|
2950
|
-
".continueignore",
|
|
2951
|
-
".copilotignore",
|
|
2952
|
-
".windsurfignore",
|
|
2953
|
-
".junieignore",
|
|
2954
|
-
".kiroignore",
|
|
2955
|
-
".rooignore",
|
|
2956
|
-
".antigravityignore"
|
|
2957
|
-
]);
|
|
2958
|
-
function shouldNormalizeLineEndings(path) {
|
|
2959
|
-
const ext = extname(path).toLowerCase();
|
|
2960
|
-
if (ext.length > 0) return TEXT_EXTENSIONS.has(ext);
|
|
2961
|
-
const base = basename(path).toLowerCase();
|
|
2962
|
-
return TEXT_DOTFILES.has(base);
|
|
4432
|
+
function commandProgram(command) {
|
|
4433
|
+
const m = /^\s*(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(command);
|
|
4434
|
+
return m?.[1] ?? m?.[2] ?? m?.[3] ?? "";
|
|
2963
4435
|
}
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
".
|
|
2968
|
-
".gif",
|
|
2969
|
-
".bmp",
|
|
2970
|
-
".ico",
|
|
2971
|
-
".webp",
|
|
2972
|
-
".avif",
|
|
2973
|
-
".tiff",
|
|
2974
|
-
".pdf",
|
|
2975
|
-
".zip",
|
|
2976
|
-
".gz",
|
|
2977
|
-
".tgz",
|
|
2978
|
-
".bz2",
|
|
2979
|
-
".xz",
|
|
2980
|
-
".7z",
|
|
2981
|
-
".rar",
|
|
2982
|
-
".jar",
|
|
2983
|
-
".woff",
|
|
2984
|
-
".woff2",
|
|
2985
|
-
".ttf",
|
|
2986
|
-
".otf",
|
|
2987
|
-
".eot",
|
|
2988
|
-
".mp3",
|
|
2989
|
-
".mp4",
|
|
2990
|
-
".wav",
|
|
2991
|
-
".ogg",
|
|
2992
|
-
".webm",
|
|
2993
|
-
".mov",
|
|
2994
|
-
".wasm",
|
|
2995
|
-
".bin",
|
|
2996
|
-
".dat",
|
|
2997
|
-
".db",
|
|
2998
|
-
".sqlite",
|
|
2999
|
-
".so",
|
|
3000
|
-
".dylib",
|
|
3001
|
-
".dll",
|
|
3002
|
-
".exe",
|
|
3003
|
-
".class",
|
|
3004
|
-
".pyc",
|
|
3005
|
-
// Office and design documents are zip or proprietary containers.
|
|
3006
|
-
".xlsx",
|
|
3007
|
-
".xls",
|
|
3008
|
-
".docx",
|
|
3009
|
-
".doc",
|
|
3010
|
-
".pptx",
|
|
3011
|
-
".ppt",
|
|
3012
|
-
".odt",
|
|
3013
|
-
".ods",
|
|
3014
|
-
".psd",
|
|
3015
|
-
".ai",
|
|
3016
|
-
".sketch",
|
|
3017
|
-
".fig",
|
|
3018
|
-
".heic",
|
|
3019
|
-
".heif",
|
|
3020
|
-
// Archives, columnar data, models and compressed payloads.
|
|
3021
|
-
".tar",
|
|
3022
|
-
".zst",
|
|
3023
|
-
".br",
|
|
3024
|
-
".lz4",
|
|
3025
|
-
".parquet",
|
|
3026
|
-
".avro",
|
|
3027
|
-
".orc",
|
|
3028
|
-
".pkl",
|
|
3029
|
-
".npy",
|
|
3030
|
-
".npz",
|
|
3031
|
-
".onnx",
|
|
3032
|
-
".pt",
|
|
3033
|
-
".safetensors",
|
|
3034
|
-
".gguf",
|
|
3035
|
-
".sqlite3",
|
|
3036
|
-
".avi",
|
|
3037
|
-
".mkv",
|
|
3038
|
-
".flac",
|
|
3039
|
-
".aac",
|
|
3040
|
-
".m4a",
|
|
3041
|
-
".ttc"
|
|
3042
|
-
]);
|
|
3043
|
-
function isBinaryPayloadPath(path) {
|
|
3044
|
-
return BINARY_EXTENSIONS.has(extname(path).toLowerCase());
|
|
4436
|
+
function isTransientBinDir(dir) {
|
|
4437
|
+
const parts = dir.split(/[\\/]+/).filter((p) => p !== "");
|
|
4438
|
+
const [parent, last] = parts.slice(-2);
|
|
4439
|
+
return parts.includes("_npx") || parent === "node_modules" && last === ".bin";
|
|
3045
4440
|
}
|
|
3046
|
-
function
|
|
3047
|
-
|
|
4441
|
+
function commandLauncherExists(command, env = process.env, platform = process.platform) {
|
|
4442
|
+
const program = commandProgram(command);
|
|
4443
|
+
if (program === "") return false;
|
|
4444
|
+
if (program.includes("/") || program.includes("\\")) return existsSync(program);
|
|
4445
|
+
const win = platform === "win32";
|
|
4446
|
+
const exts = win ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
|
|
4447
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(win ? ";" : ":").filter((d) => d !== "" && !isTransientBinDir(d));
|
|
4448
|
+
return dirs.some((dir) => exts.some((ext) => existsSync(join(dir, program + ext))));
|
|
3048
4449
|
}
|
|
3049
|
-
function
|
|
3050
|
-
|
|
4450
|
+
function localBinExists(fromDir, name) {
|
|
4451
|
+
for (let dir = resolve(fromDir); ; dir = dirname(dir)) {
|
|
4452
|
+
const bin = join(dir, "node_modules", ".bin");
|
|
4453
|
+
if (existsSync(join(bin, name)) || existsSync(join(bin, `${name}.cmd`))) return true;
|
|
4454
|
+
if (dirname(dir) === dir) return false;
|
|
4455
|
+
}
|
|
3051
4456
|
}
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
4457
|
+
|
|
4458
|
+
// src/lessons/merge-driver-setup.ts
|
|
4459
|
+
var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";
|
|
4460
|
+
var LESSONS_GITATTRIBUTES_ENTRY = `${LESSONS_GRAPH_PATH} merge=${LESSONS_MERGE_DRIVER}`;
|
|
4461
|
+
var DRIVER_KEY = `merge.${LESSONS_MERGE_DRIVER}.driver`;
|
|
4462
|
+
var NAME_KEY = `merge.${LESSONS_MERGE_DRIVER}.name`;
|
|
4463
|
+
var DRIVER_NAME = "agentsmesh lessons union";
|
|
4464
|
+
function lessonsMergeDriverCommand(invocation) {
|
|
4465
|
+
return `${invocation.replaceAll("\\", "/")} lessons merge-driver %O %A %B`;
|
|
4466
|
+
}
|
|
4467
|
+
var NPX_INVOCATION = "npx --no --offline agentsmesh";
|
|
4468
|
+
var NPX_COMMAND = lessonsMergeDriverCommand(NPX_INVOCATION);
|
|
4469
|
+
var BARE_COMMAND = lessonsMergeDriverCommand("agentsmesh");
|
|
4470
|
+
var OWN_COMMANDS = /* @__PURE__ */ new Set([BARE_COMMAND, NPX_COMMAND]);
|
|
4471
|
+
function launchable(command, env) {
|
|
4472
|
+
const npxMissing = command === NPX_COMMAND && !commandLauncherExists(NPX_COMMAND, env);
|
|
4473
|
+
return npxMissing && commandLauncherExists(BARE_COMMAND, env) ? BARE_COMMAND : command;
|
|
4474
|
+
}
|
|
4475
|
+
function configValue(git, root, key) {
|
|
4476
|
+
const r = git(root, ["config", "--get", key]);
|
|
4477
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
4478
|
+
}
|
|
4479
|
+
function launchProblem(git, projectRoot, command, env) {
|
|
4480
|
+
if (!commandLauncherExists(command, env)) {
|
|
4481
|
+
return `\`${commandProgram(command)}\` is not installed on PATH (npx and package-script bin folders do not count), so git could not start the driver; install agentsmesh globally or as a project devDependency`;
|
|
4482
|
+
}
|
|
4483
|
+
if (command !== NPX_COMMAND) return null;
|
|
4484
|
+
const top = git(projectRoot, ["rev-parse", "--show-toplevel"]);
|
|
4485
|
+
const repoRoot = top.status === 0 ? top.stdout.trim() : projectRoot;
|
|
4486
|
+
if (localBinExists(repoRoot, "agentsmesh") || commandLauncherExists("agentsmesh", env)) {
|
|
4487
|
+
return null;
|
|
4488
|
+
}
|
|
4489
|
+
return `git runs the driver from the repository root (${repoRoot.replaceAll("\\", "/")}), where \`${NPX_INVOCATION}\` cannot find agentsmesh; add agentsmesh to the devDependencies of the root package.json and install, or install agentsmesh globally`;
|
|
3055
4490
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
const
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
4491
|
+
function ensureLessonsMergeDriver(projectRoot, options = {}) {
|
|
4492
|
+
const git = options.git ?? runGit;
|
|
4493
|
+
const env = options.env ?? process.env;
|
|
4494
|
+
const command = launchable(
|
|
4495
|
+
lessonsMergeDriverCommand(options.invocation ?? agentsmeshInvocation(projectRoot)),
|
|
4496
|
+
env
|
|
4497
|
+
);
|
|
4498
|
+
const attr = git(projectRoot, ["check-attr", "merge", "--", LESSONS_GRAPH_PATH]);
|
|
4499
|
+
if (attr.status !== 0 || !attr.stdout.trim().endsWith(`: merge: ${LESSONS_MERGE_DRIVER}`)) {
|
|
4500
|
+
return { status: "skipped", command };
|
|
4501
|
+
}
|
|
4502
|
+
const existing = configValue(git, projectRoot, DRIVER_KEY);
|
|
4503
|
+
if (existing !== null && existing !== command && !OWN_COMMANDS.has(existing)) {
|
|
4504
|
+
return { status: "custom", command, existing };
|
|
4505
|
+
}
|
|
4506
|
+
const reason = existing === command ? null : launchProblem(git, projectRoot, command, env);
|
|
4507
|
+
if (reason !== null) return { status: "failed", command, reason };
|
|
4508
|
+
const writes = [];
|
|
4509
|
+
if (existing !== command) writes.push([DRIVER_KEY, command]);
|
|
4510
|
+
if (configValue(git, projectRoot, NAME_KEY) === null) writes.push([NAME_KEY, DRIVER_NAME]);
|
|
4511
|
+
for (const [key, value] of writes) {
|
|
4512
|
+
const r = git(projectRoot, ["config", "--local", key, value]);
|
|
4513
|
+
if (r.status !== 0) {
|
|
4514
|
+
const detail = r.stderr.trim() || `exit ${r.status}`;
|
|
4515
|
+
const reason2 = `git config failed (${detail}); run: git config ${DRIVER_KEY} "${command}"`;
|
|
4516
|
+
return { status: "failed", command, reason: reason2 };
|
|
3069
4517
|
}
|
|
3070
4518
|
}
|
|
4519
|
+
if (existing === command) return { status: "unchanged", command };
|
|
4520
|
+
return { status: existing === null ? "configured" : "updated", command };
|
|
4521
|
+
}
|
|
4522
|
+
var RECALL_HOOK_TEAM_HINT = "Lessons recall hooks call a global agentsmesh: teammates without a global install will not get lesson recall; add agentsmesh as a devDependency and re-run 'agentsmesh init --lessons'.";
|
|
4523
|
+
function recallHookTeamHint(projectRoot) {
|
|
4524
|
+
const wired = wiredRecallCommands(projectRoot);
|
|
4525
|
+
if (wired.length === 0) return null;
|
|
4526
|
+
const expected = recallHookCommand(projectRoot);
|
|
4527
|
+
const stale = wired.find((command) => command !== expected);
|
|
4528
|
+
if (stale !== void 0) {
|
|
4529
|
+
return `Lessons recall hooks run \`${stale}\`, but this project now calls \`${expected}\`; re-run 'agentsmesh init --lessons' to update them.`;
|
|
4530
|
+
}
|
|
4531
|
+
if (!existsSync(join(projectRoot, "package.json"))) return null;
|
|
4532
|
+
return expected === RECALL_HOOK_COMMAND ? RECALL_HOOK_TEAM_HINT : null;
|
|
4533
|
+
}
|
|
4534
|
+
function wiredRecallCommands(projectRoot) {
|
|
4535
|
+
try {
|
|
4536
|
+
const hooks = parse(
|
|
4537
|
+
readFileSync(join(projectRoot, ".agentsmesh", "hooks.yaml"), "utf8")
|
|
4538
|
+
);
|
|
4539
|
+
return Object.values(hooks ?? {}).filter(Array.isArray).flat().map((entry) => entry?.command).filter(isManagedRecallCommand).map((command) => command.trim());
|
|
4540
|
+
} catch {
|
|
4541
|
+
return [];
|
|
4542
|
+
}
|
|
3071
4543
|
}
|
|
3072
4544
|
|
|
3073
4545
|
// src/utils/filesystem/fs.ts
|
|
@@ -3313,7 +4785,8 @@ At least one _effective_ trigger is required (or \`--scope always\` for a univer
|
|
|
3313
4785
|
the capture is rejected (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
|
|
3314
4786
|
\`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
|
|
3315
4787
|
\`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
|
|
3316
|
-
deprecate, merge, untrigger, strip-markers, prune, journal, validate, stats, import-md.
|
|
4788
|
+
deprecate, merge, untrigger, strip-markers, prune, journal, validate, resolve, stats, import-md.
|
|
4789
|
+
A git merge conflict in \`lessons.json\` \u2192 run \`agentsmesh lessons resolve\`; never hand-edit it.
|
|
3317
4790
|
|
|
3318
4791
|
### Rationalization Prevention \u2014 these excuses mean STOP
|
|
3319
4792
|
|
|
@@ -3357,11 +4830,14 @@ async function scaffoldLessons(projectRoot) {
|
|
|
3357
4830
|
const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
|
|
3358
4831
|
toRelPath(projectRoot, recallLogPath(projectRoot)),
|
|
3359
4832
|
toRelPath(projectRoot, captureLogPath(projectRoot)),
|
|
3360
|
-
toRelPath(projectRoot, outcomeLogPath(projectRoot))
|
|
4833
|
+
toRelPath(projectRoot, outcomeLogPath(projectRoot)),
|
|
4834
|
+
`${toRelPath(projectRoot, lessonsLockPath(projectRoot))}/`,
|
|
4835
|
+
`${toRelPath(projectRoot, paths.base)}/*.tmp`
|
|
3361
4836
|
]);
|
|
3362
4837
|
const gitattributesUpdated = await ensureGitattributesEntries(projectRoot, [
|
|
3363
4838
|
LESSONS_GITATTRIBUTES_ENTRY
|
|
3364
4839
|
]);
|
|
4840
|
+
const mergeDriver = ensureLessonsMergeDriver(projectRoot);
|
|
3365
4841
|
return {
|
|
3366
4842
|
created,
|
|
3367
4843
|
updated,
|
|
@@ -3369,7 +4845,9 @@ async function scaffoldLessons(projectRoot) {
|
|
|
3369
4845
|
rootRuleUpdated,
|
|
3370
4846
|
gitignoreUpdated,
|
|
3371
4847
|
gitattributesUpdated,
|
|
3372
|
-
recallHookInjected
|
|
4848
|
+
recallHookInjected,
|
|
4849
|
+
mergeDriver,
|
|
4850
|
+
recallHookTeamHint: recallHookTeamHint(projectRoot)
|
|
3373
4851
|
};
|
|
3374
4852
|
}
|
|
3375
4853
|
function seedLessonsSkill(projectRoot, created, updated, skipped) {
|