agentsmesh 0.21.0 → 0.23.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 +244 -0
- package/README.md +71 -9
- package/dist/canonical.d.ts +2 -2
- package/dist/canonical.js +339 -46
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +276 -223
- package/dist/engine.d.ts +5 -2
- package/dist/engine.js +1350 -87
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2993 -439
- package/dist/index.js.map +1 -1
- package/dist/init-YKxF2zpQ.d.ts +494 -0
- package/dist/lessons.d.ts +106 -0
- package/dist/lessons.js +2764 -0
- package/dist/lessons.js.map +1 -0
- package/dist/{schema-CLmR2JOb.d.ts → schema-CzaoYJlG.d.ts} +9 -1
- package/dist/{target-descriptor-CkLWz3Xk.d.ts → target-descriptor-D6vLDI1w.d.ts} +62 -2
- package/dist/targets.d.ts +3 -3
- package/dist/targets.js +293 -32
- package/dist/targets.js.map +1 -1
- package/package.json +9 -2
- package/schemas/installs.json +12 -0
package/dist/lessons.js
ADDED
|
@@ -0,0 +1,2764 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import picomatch from 'picomatch';
|
|
4
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync, statSync } from 'fs';
|
|
5
|
+
import { resolve, join, relative, sep, dirname, basename, extname } from 'path';
|
|
6
|
+
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
7
|
+
import { mkdir, rm, writeFile, readFile, stat, lstat, unlink, rename, chmod } from 'fs/promises';
|
|
8
|
+
import { tmpdir, hostname } from 'os';
|
|
9
|
+
import 'timers/promises';
|
|
10
|
+
|
|
11
|
+
// src/lessons/graph-schema.ts
|
|
12
|
+
var CURRENT_GRAPH_VERSION = 1;
|
|
13
|
+
var MAX_RULE_LENGTH = 2e3;
|
|
14
|
+
var IdSchema = z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case");
|
|
15
|
+
var DateSchema = z.string().regex(
|
|
16
|
+
/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z?)?$/,
|
|
17
|
+
"createdAt must be ISO-8601 date or datetime"
|
|
18
|
+
);
|
|
19
|
+
var TopicSchema = z.object({
|
|
20
|
+
summary: z.string().min(1, "topic summary must not be empty")
|
|
21
|
+
}).strict();
|
|
22
|
+
var TriggerKindSchema = z.enum(["file_glob", "command_pattern", "keyword"]);
|
|
23
|
+
var TriggerSchema = z.object({
|
|
24
|
+
kind: TriggerKindSchema,
|
|
25
|
+
pattern: z.string().min(1, "trigger pattern must not be empty")
|
|
26
|
+
}).strict();
|
|
27
|
+
var LessonStatusSchema = z.enum(["active", "deprecated", "superseded"]);
|
|
28
|
+
var LessonSchema = z.object({
|
|
29
|
+
rule: z.string().min(1, "lesson rule must not be empty"),
|
|
30
|
+
rationale: z.string().min(1).optional(),
|
|
31
|
+
topics: z.array(IdSchema).min(1, "lesson must reference at least one topic"),
|
|
32
|
+
triggers: z.array(IdSchema),
|
|
33
|
+
evidence: z.array(z.string().min(1)),
|
|
34
|
+
status: LessonStatusSchema,
|
|
35
|
+
supersededBy: IdSchema.optional(),
|
|
36
|
+
createdAt: DateSchema
|
|
37
|
+
}).strict();
|
|
38
|
+
var LessonsGraphSchema = z.object({
|
|
39
|
+
version: z.literal(CURRENT_GRAPH_VERSION),
|
|
40
|
+
lessons: z.record(IdSchema, LessonSchema),
|
|
41
|
+
topics: z.record(IdSchema, TopicSchema),
|
|
42
|
+
triggers: z.record(IdSchema, TriggerSchema)
|
|
43
|
+
}).strict();
|
|
44
|
+
function parseGraph(raw) {
|
|
45
|
+
return LessonsGraphSchema.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
function normalizeRule(rule) {
|
|
48
|
+
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
function union(base, extra) {
|
|
51
|
+
const out = [...base];
|
|
52
|
+
for (const item of extra) if (!out.includes(item)) out.push(item);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function mergeTriggers(graph, spec) {
|
|
56
|
+
const requested = [
|
|
57
|
+
// Normalize `\` → `/` so a Windows-shaped glob matches: recall relativizes
|
|
58
|
+
// every `--file` to forward slashes (normalizeRecallFile), so a backslash
|
|
59
|
+
// pattern stored raw would silently never fire. Normalizing here also lets
|
|
60
|
+
// a backslash pattern dedupe against the forward-slash node it equals.
|
|
61
|
+
...(spec.files ?? []).map(
|
|
62
|
+
(p) => ({ kind: "file_glob", pattern: p.replaceAll("\\", "/") })
|
|
63
|
+
),
|
|
64
|
+
...(spec.commands ?? []).map((p) => ({ kind: "command_pattern", pattern: p })),
|
|
65
|
+
...(spec.keywords ?? []).map((p) => ({ kind: "keyword", pattern: p }))
|
|
66
|
+
];
|
|
67
|
+
const reverseLookup = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
69
|
+
reverseLookup.set(triggerKey(trigger), id);
|
|
70
|
+
}
|
|
71
|
+
const triggerIds = [];
|
|
72
|
+
const newTriggerIds = [];
|
|
73
|
+
for (const spec2 of requested) {
|
|
74
|
+
const key = triggerKey(spec2);
|
|
75
|
+
const existing = reverseLookup.get(key);
|
|
76
|
+
if (existing !== void 0) {
|
|
77
|
+
if (!triggerIds.includes(existing)) triggerIds.push(existing);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const id = makeTriggerId(spec2);
|
|
81
|
+
graph.triggers[id] = { kind: spec2.kind, pattern: spec2.pattern };
|
|
82
|
+
reverseLookup.set(key, id);
|
|
83
|
+
triggerIds.push(id);
|
|
84
|
+
newTriggerIds.push(id);
|
|
85
|
+
}
|
|
86
|
+
return { triggerIds, newTriggerIds };
|
|
87
|
+
}
|
|
88
|
+
function triggerKey(t) {
|
|
89
|
+
return `${t.kind}|${t.pattern}`;
|
|
90
|
+
}
|
|
91
|
+
var TRIGGER_PREFIX = {
|
|
92
|
+
file_glob: "glob",
|
|
93
|
+
command_pattern: "cmd",
|
|
94
|
+
keyword: "kw"
|
|
95
|
+
};
|
|
96
|
+
function makeTriggerId(spec) {
|
|
97
|
+
const hash = createHash("sha1").update(triggerKey(spec)).digest("hex").slice(0, 8);
|
|
98
|
+
return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
|
|
99
|
+
}
|
|
100
|
+
function makeLessonId(graph, topic, ruleKey) {
|
|
101
|
+
const slug = ruleToSlug(ruleKey);
|
|
102
|
+
const base = slug.length > 0 ? `${topic}-${slug}` : `${topic}-${createHash("sha1").update(ruleKey).digest("hex").slice(0, 8)}`;
|
|
103
|
+
let candidate = base;
|
|
104
|
+
let i = 2;
|
|
105
|
+
while (graph.lessons[candidate] !== void 0) {
|
|
106
|
+
candidate = `${base}-${i}`;
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
function ruleToSlug(rule) {
|
|
112
|
+
const words = rule.replace(/[^a-z0-9 ]+/g, " ").split(/\s+/).filter((w) => w.length > 0).slice(0, 5);
|
|
113
|
+
return words.join("-").slice(0, 40).replace(/-+$/, "");
|
|
114
|
+
}
|
|
115
|
+
function todayIso() {
|
|
116
|
+
const now = /* @__PURE__ */ new Date();
|
|
117
|
+
const y = now.getUTCFullYear();
|
|
118
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
119
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
120
|
+
return `${y}-${m}-${d}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/lessons/add-errors.ts
|
|
124
|
+
var UnknownTopicError = class extends Error {
|
|
125
|
+
constructor(topic) {
|
|
126
|
+
super(`Unknown topic: ${topic}. Pass allowNewTopic + topicSummary to create it.`);
|
|
127
|
+
this.topic = topic;
|
|
128
|
+
this.name = "UnknownTopicError";
|
|
129
|
+
}
|
|
130
|
+
code = "UNKNOWN_TOPIC";
|
|
131
|
+
};
|
|
132
|
+
var RuleTooLongError = class extends Error {
|
|
133
|
+
constructor(length, max) {
|
|
134
|
+
super(
|
|
135
|
+
`Lesson rule is ${length} characters (max ${max}). A rule should be one imperative sentence \u2014 trim it to the essential instruction, or split it into separate lessons.`
|
|
136
|
+
);
|
|
137
|
+
this.length = length;
|
|
138
|
+
this.max = max;
|
|
139
|
+
this.name = "RuleTooLongError";
|
|
140
|
+
}
|
|
141
|
+
code = "OVERSIZED_RULE";
|
|
142
|
+
};
|
|
143
|
+
var NoTriggerError = class extends Error {
|
|
144
|
+
code = "NO_TRIGGER";
|
|
145
|
+
constructor() {
|
|
146
|
+
super(
|
|
147
|
+
"A lesson needs at least one trigger to be recallable. Pass --trigger-file <glob> (preferred), --trigger-cmd <regex>, or --trigger-kw <text>."
|
|
148
|
+
);
|
|
149
|
+
this.name = "NoTriggerError";
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var UnrecallableLessonError = class extends Error {
|
|
153
|
+
constructor(deadTriggers) {
|
|
154
|
+
super(
|
|
155
|
+
"This capture would create a lesson with no effective trigger \u2014 every trigger is dead on the mandatory --file/--cmd recall path, so the lesson could never be recalled there:\n" + deadTriggers.map((t) => ` \u2022 ${t.kind} "${t.pattern}" \u2014 ${t.reason}`).join("\n") + '\nFix: add a precise --trigger-file <glob> (preferred) or a valid --trigger-cmd <regex>; for a keyword, drop the stopwords (e.g. "state art" not "state of the art").'
|
|
156
|
+
);
|
|
157
|
+
this.deadTriggers = deadTriggers;
|
|
158
|
+
this.name = "UnrecallableLessonError";
|
|
159
|
+
}
|
|
160
|
+
code = "UNRECALLABLE_LESSON";
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/lessons/ranking-text.ts
|
|
164
|
+
var K1 = 1.5;
|
|
165
|
+
var B = 0.75;
|
|
166
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
167
|
+
"the",
|
|
168
|
+
"a",
|
|
169
|
+
"an",
|
|
170
|
+
"to",
|
|
171
|
+
"of",
|
|
172
|
+
"in",
|
|
173
|
+
"and",
|
|
174
|
+
"or",
|
|
175
|
+
"for",
|
|
176
|
+
"is",
|
|
177
|
+
"on",
|
|
178
|
+
"at",
|
|
179
|
+
"with",
|
|
180
|
+
"be",
|
|
181
|
+
"as",
|
|
182
|
+
"it",
|
|
183
|
+
"that",
|
|
184
|
+
"this",
|
|
185
|
+
"its",
|
|
186
|
+
"must"
|
|
187
|
+
]);
|
|
188
|
+
function tokenize(text) {
|
|
189
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
190
|
+
}
|
|
191
|
+
function queryTerms(query) {
|
|
192
|
+
const parts = [];
|
|
193
|
+
if (query.keyword !== void 0) parts.push(query.keyword);
|
|
194
|
+
if (query.file !== void 0) parts.push(query.file);
|
|
195
|
+
if (query.command !== void 0) parts.push(query.command);
|
|
196
|
+
return tokenize(parts.join(" "));
|
|
197
|
+
}
|
|
198
|
+
function buildCorpus(graph) {
|
|
199
|
+
const docs = [];
|
|
200
|
+
const df = /* @__PURE__ */ new Map();
|
|
201
|
+
let total = 0;
|
|
202
|
+
let n = 0;
|
|
203
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
204
|
+
if (lesson.status !== "active") continue;
|
|
205
|
+
const toks = tokenize(lesson.rule);
|
|
206
|
+
n += 1;
|
|
207
|
+
total += toks.length;
|
|
208
|
+
docs.push(toks.length);
|
|
209
|
+
for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
210
|
+
}
|
|
211
|
+
const N = Math.max(n, 1);
|
|
212
|
+
const idf = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
|
|
214
|
+
return { idf, avgdl: total / N || 1 };
|
|
215
|
+
}
|
|
216
|
+
function bm25(terms, ruleText, corpus) {
|
|
217
|
+
const toks = tokenize(ruleText);
|
|
218
|
+
const dl = toks.length || 1;
|
|
219
|
+
const tf = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
221
|
+
let score = 0;
|
|
222
|
+
for (const t of new Set(terms)) {
|
|
223
|
+
const f = tf.get(t) ?? 0;
|
|
224
|
+
if (f === 0) continue;
|
|
225
|
+
const idf = corpus.idf.get(t);
|
|
226
|
+
score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
|
|
227
|
+
}
|
|
228
|
+
return score;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/lessons/keyword-signal.ts
|
|
232
|
+
var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
|
|
233
|
+
function isLowSignalKeyword(pattern) {
|
|
234
|
+
return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
|
|
235
|
+
}
|
|
236
|
+
function splitRawTokens(pattern) {
|
|
237
|
+
return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
238
|
+
}
|
|
239
|
+
function keywordNeedleLosesTokens(pattern) {
|
|
240
|
+
const raw = splitRawTokens(pattern);
|
|
241
|
+
if (raw.length < 2) return false;
|
|
242
|
+
return tokenize(pattern).length !== raw.length;
|
|
243
|
+
}
|
|
244
|
+
function activeTriggerIds(graph) {
|
|
245
|
+
const ids = /* @__PURE__ */ new Set();
|
|
246
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
247
|
+
if (lesson.status !== "active") continue;
|
|
248
|
+
for (const t of lesson.triggers) ids.add(t);
|
|
249
|
+
}
|
|
250
|
+
return ids;
|
|
251
|
+
}
|
|
252
|
+
function deadFileGlobIds(graph, knownPaths) {
|
|
253
|
+
const active = activeTriggerIds(graph);
|
|
254
|
+
const paths = [...knownPaths];
|
|
255
|
+
const dead = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
257
|
+
if (trigger.kind !== "file_glob") continue;
|
|
258
|
+
if (!active.has(triggerId)) continue;
|
|
259
|
+
const isMatch = picomatch(trigger.pattern, { dot: true });
|
|
260
|
+
if (!paths.some((p) => isMatch(p))) dead.add(triggerId);
|
|
261
|
+
}
|
|
262
|
+
return dead;
|
|
263
|
+
}
|
|
264
|
+
function collectDeadFileGlobs(graph, findings, knownPaths) {
|
|
265
|
+
for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
|
|
266
|
+
findings.push({
|
|
267
|
+
level: "warning",
|
|
268
|
+
code: "DEAD_FILE_GLOB",
|
|
269
|
+
message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file in the working tree \u2014 the lesson is unreachable via this trigger (a rename likely moved the path). Re-point it at the current path, or detach it with \`lessons untrigger\`, or run \`lessons prune --apply\`.`,
|
|
270
|
+
triggerId
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
|
|
275
|
+
function collectRunnerAnchoredPatterns(graph, findings) {
|
|
276
|
+
const active = activeTriggerIds(graph);
|
|
277
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
278
|
+
if (trigger.kind !== "command_pattern") continue;
|
|
279
|
+
if (!active.has(triggerId)) continue;
|
|
280
|
+
if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
|
|
281
|
+
findings.push({
|
|
282
|
+
level: "warning",
|
|
283
|
+
code: "RUNNER_ANCHORED_PATTERN",
|
|
284
|
+
message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) is anchored to one runner \u2014 it won't fire for the same task via another runner (e.g. \`npx\` vs \`pnpm\`). Drop the \`^<runner>\` anchor and key on the task (e.g. \`\\bvitest\\b\`).`,
|
|
285
|
+
triggerId
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/lessons/capture-guardrails.ts
|
|
291
|
+
var NEAR_DUPLICATE_THRESHOLD = 0.6;
|
|
292
|
+
var MAX_RECOMMENDED_TRIGGERS = 8;
|
|
293
|
+
function isBroadGlob(pattern) {
|
|
294
|
+
const p = pattern.trim();
|
|
295
|
+
if (p === "*" || p === "**") return true;
|
|
296
|
+
if (!p.includes("**")) return false;
|
|
297
|
+
const basename5 = p.slice(p.lastIndexOf("/") + 1);
|
|
298
|
+
return basename5.startsWith("*");
|
|
299
|
+
}
|
|
300
|
+
function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
301
|
+
const lesson = graph.lessons[lessonId];
|
|
302
|
+
if (lesson === void 0) return [];
|
|
303
|
+
const warnings = [];
|
|
304
|
+
if (lesson.triggers.length > MAX_RECOMMENDED_TRIGGERS) {
|
|
305
|
+
warnings.push({
|
|
306
|
+
code: "OVERSIZED_LESSON_TRIGGERS",
|
|
307
|
+
message: `Lesson "${lessonId}" has ${lesson.triggers.length} triggers (recommended \u2264 ${MAX_RECOMMENDED_TRIGGERS}); broad trigger sets fire on too many edits and dilute recall \u2014 prefer a few specific triggers.`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const triggers = lesson.triggers.map((id) => graph.triggers[id]).filter((t) => t !== void 0);
|
|
311
|
+
const broad = triggers.filter((t) => t.kind === "file_glob" && isBroadGlob(t.pattern)).map((t) => t.pattern);
|
|
312
|
+
if (broad.length > 0) {
|
|
313
|
+
warnings.push({
|
|
314
|
+
code: "BROAD_GLOB_TRIGGER",
|
|
315
|
+
message: `Lesson "${lessonId}" has broad file glob(s) (${broad.join(", ")}) that match large swaths of the tree; prefer a path specific to the lesson.`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (triggers.length > 0 && triggers.every((t) => t.kind === "keyword")) {
|
|
319
|
+
warnings.push({
|
|
320
|
+
code: "KEYWORD_ONLY_LESSON",
|
|
321
|
+
message: `Lesson "${lessonId}" has only keyword triggers; mandatory --file/--cmd recall surfaces these only when the keyword appears as a path/command token, so it fires less reliably \u2014 add a file_glob or command_pattern trigger for precise recall.`
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const lowSignal = triggers.filter((t) => t.kind === "keyword" && isLowSignalKeyword(t.pattern)).map((t) => t.pattern);
|
|
325
|
+
if (lowSignal.length > 0) {
|
|
326
|
+
warnings.push({
|
|
327
|
+
code: "LOW_SIGNAL_KEYWORD",
|
|
328
|
+
message: `Lesson "${lessonId}" has long keyword trigger(s) (${lowSignal.join(", ")}); recall matches a keyword only as a substring of --keyword or a contiguous token-run in the file/command, so a pattern past ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens rarely fires \u2014 use a short distinctive phrase.`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const stopworded = triggers.filter((t) => t.kind === "keyword" && keywordNeedleLosesTokens(t.pattern)).map((t) => t.pattern);
|
|
332
|
+
if (stopworded.length > 0) {
|
|
333
|
+
warnings.push({
|
|
334
|
+
code: "STOPWORD_KEYWORD",
|
|
335
|
+
message: `Lesson "${lessonId}" has keyword trigger(s) containing stopwords/short words (${stopworded.join(", ")}); recall filters them from the pattern but NOT from the file/command text, so the phrase can never match contiguously on the --file/--cmd path \u2014 drop the stopwords (e.g. "state art" instead of "state of the art").`
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (knownPaths !== void 0) {
|
|
339
|
+
const dead = deadFileGlobIds(graph, knownPaths);
|
|
340
|
+
const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
|
|
341
|
+
if (deadHere.length > 0) {
|
|
342
|
+
warnings.push({
|
|
343
|
+
code: "DEAD_GLOB",
|
|
344
|
+
message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file in the working tree \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return warnings;
|
|
349
|
+
}
|
|
350
|
+
function nearDuplicateWarning(graph, lessonId) {
|
|
351
|
+
const subject = graph.lessons[lessonId];
|
|
352
|
+
if (subject === void 0) return null;
|
|
353
|
+
const subjectTokens = new Set(tokenize(subject.rule));
|
|
354
|
+
if (subjectTokens.size === 0) return null;
|
|
355
|
+
let best = null;
|
|
356
|
+
for (const [id, other] of Object.entries(graph.lessons)) {
|
|
357
|
+
if (id === lessonId || other.status !== "active") continue;
|
|
358
|
+
const otherTokens = new Set(tokenize(other.rule));
|
|
359
|
+
if (otherTokens.size === 0) continue;
|
|
360
|
+
const score = jaccard(subjectTokens, otherTokens);
|
|
361
|
+
if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
|
|
362
|
+
best = { id, score };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (best === null) return null;
|
|
366
|
+
return {
|
|
367
|
+
code: "NEAR_DUPLICATE_LESSON",
|
|
368
|
+
message: `Lesson "${lessonId}" closely resembles active lesson "${best.id}" (~${Math.round(best.score * 100)}% token overlap); consider updating "${best.id}" instead of adding a paraphrase (recall would surface both).`
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function jaccard(a, b) {
|
|
372
|
+
let intersection = 0;
|
|
373
|
+
for (const t of a) if (b.has(t)) intersection += 1;
|
|
374
|
+
return intersection / (a.size + b.size - intersection);
|
|
375
|
+
}
|
|
376
|
+
var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
|
|
377
|
+
function graphFilePath(projectRoot) {
|
|
378
|
+
return resolve(projectRoot, GRAPH_REL_PATH);
|
|
379
|
+
}
|
|
380
|
+
function loadLessonsGraph(projectRoot) {
|
|
381
|
+
const raw = readFileSync(graphFilePath(projectRoot), "utf8");
|
|
382
|
+
return parseGraph(JSON.parse(raw));
|
|
383
|
+
}
|
|
384
|
+
function tryLoadLessonsGraph(projectRoot) {
|
|
385
|
+
if (!existsSync(graphFilePath(projectRoot))) return null;
|
|
386
|
+
return loadLessonsGraph(projectRoot);
|
|
387
|
+
}
|
|
388
|
+
function loadLessonsGraphResilient(projectRoot) {
|
|
389
|
+
const path = graphFilePath(projectRoot);
|
|
390
|
+
if (!existsSync(path)) return { status: "absent", graph: null };
|
|
391
|
+
try {
|
|
392
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
393
|
+
const version = parsed?.version;
|
|
394
|
+
if (typeof version === "number" && version > CURRENT_GRAPH_VERSION) {
|
|
395
|
+
return { status: "newer-version", graph: null, version };
|
|
396
|
+
}
|
|
397
|
+
return { status: "ok", graph: parseGraph(parsed) };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return {
|
|
400
|
+
status: "corrupt",
|
|
401
|
+
graph: null,
|
|
402
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function saveLessonsGraph(projectRoot, graph) {
|
|
407
|
+
const path = graphFilePath(projectRoot);
|
|
408
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
409
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
410
|
+
writeFileSync(tmp, serializeGraph(graph), "utf8");
|
|
411
|
+
renameSync(tmp, path);
|
|
412
|
+
}
|
|
413
|
+
function serializeGraph(graph) {
|
|
414
|
+
return `${JSON.stringify(canonicalize(graph), null, 2)}
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
function canonicalize(value) {
|
|
418
|
+
if (value === null) return null;
|
|
419
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
420
|
+
if (typeof value === "object") {
|
|
421
|
+
const entries = Object.entries(value).sort(
|
|
422
|
+
([a], [b]) => a < b ? -1 : 1
|
|
423
|
+
);
|
|
424
|
+
const out = {};
|
|
425
|
+
for (const [k, v] of entries) out[k] = canonicalize(v);
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
var LegacyTriggersSchema = z.object({
|
|
431
|
+
file_globs: z.array(z.string()),
|
|
432
|
+
command_patterns: z.array(z.string()),
|
|
433
|
+
keywords: z.array(z.string())
|
|
434
|
+
}).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
|
|
435
|
+
message: "cluster must declare at least one trigger of any type"
|
|
436
|
+
});
|
|
437
|
+
var LegacyClusterSchema = z.object({
|
|
438
|
+
topic: z.string().regex(/^[a-z0-9-]+$/),
|
|
439
|
+
file: z.string().regex(/\.md$/),
|
|
440
|
+
summary: z.string().min(1),
|
|
441
|
+
triggers: LegacyTriggersSchema
|
|
442
|
+
});
|
|
443
|
+
var LegacyIndexSchema = z.object({
|
|
444
|
+
version: z.literal(1),
|
|
445
|
+
clusters: z.array(LegacyClusterSchema)
|
|
446
|
+
});
|
|
447
|
+
function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
|
|
448
|
+
const specs = [
|
|
449
|
+
...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
|
|
450
|
+
...cluster.triggers.command_patterns.map(
|
|
451
|
+
(p) => ({ kind: "command_pattern", pattern: p })
|
|
452
|
+
),
|
|
453
|
+
...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
|
|
454
|
+
];
|
|
455
|
+
const ids = [];
|
|
456
|
+
for (const spec of specs) {
|
|
457
|
+
const key = `${spec.kind}|${spec.pattern}`;
|
|
458
|
+
let id = triggerIdByKey.get(key);
|
|
459
|
+
if (id === void 0) {
|
|
460
|
+
id = makeTriggerId2(spec);
|
|
461
|
+
triggerIdByKey.set(key, id);
|
|
462
|
+
triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
|
|
463
|
+
}
|
|
464
|
+
if (!ids.includes(id)) ids.push(id);
|
|
465
|
+
}
|
|
466
|
+
return ids;
|
|
467
|
+
}
|
|
468
|
+
var TRIGGER_PREFIX2 = {
|
|
469
|
+
file_glob: "glob",
|
|
470
|
+
command_pattern: "cmd",
|
|
471
|
+
keyword: "kw"
|
|
472
|
+
};
|
|
473
|
+
function makeTriggerId2(spec) {
|
|
474
|
+
const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
|
|
475
|
+
return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
|
|
476
|
+
}
|
|
477
|
+
var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
|
|
478
|
+
var NEXT_HEADING_RE = /^##\s+/;
|
|
479
|
+
var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
|
|
480
|
+
var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
|
|
481
|
+
var EVIDENCE_REF_RE = /L\d+/g;
|
|
482
|
+
function parseRulesSection(markdown) {
|
|
483
|
+
const lines = markdown.split(/\r?\n/);
|
|
484
|
+
let inRules = false;
|
|
485
|
+
const rules = [];
|
|
486
|
+
for (const line of lines) {
|
|
487
|
+
if (!inRules) {
|
|
488
|
+
if (RULE_HEADING_RE.test(line)) inRules = true;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (NEXT_HEADING_RE.test(line)) break;
|
|
492
|
+
const m = RULE_LINE_RE.exec(line);
|
|
493
|
+
if (m === null) continue;
|
|
494
|
+
const ruleIndex = Number(m[1]);
|
|
495
|
+
let body = m[2];
|
|
496
|
+
const evidence = [];
|
|
497
|
+
let tail = EVIDENCE_TAIL_RE.exec(body);
|
|
498
|
+
while (tail !== null) {
|
|
499
|
+
const refs = tail[1];
|
|
500
|
+
const matches = refs.match(EVIDENCE_REF_RE);
|
|
501
|
+
if (matches !== null) evidence.unshift(...matches);
|
|
502
|
+
body = body.slice(0, tail.index).trimEnd();
|
|
503
|
+
tail = EVIDENCE_TAIL_RE.exec(body);
|
|
504
|
+
}
|
|
505
|
+
rules.push({ index: ruleIndex, body, evidence });
|
|
506
|
+
}
|
|
507
|
+
return rules;
|
|
508
|
+
}
|
|
509
|
+
var LEGACY_ARTIFACT_REL = [
|
|
510
|
+
"index.yaml",
|
|
511
|
+
"journal.md",
|
|
512
|
+
"journal.legacy.md",
|
|
513
|
+
"topics",
|
|
514
|
+
"distill-ledger.yaml",
|
|
515
|
+
"distill-proposal.md"
|
|
516
|
+
];
|
|
517
|
+
function deleteLegacyArtifacts(baseDir) {
|
|
518
|
+
const deleted = [];
|
|
519
|
+
for (const rel of LEGACY_ARTIFACT_REL) {
|
|
520
|
+
const abs = join(baseDir, rel);
|
|
521
|
+
if (!existsSync(abs)) continue;
|
|
522
|
+
rmSync(abs, { recursive: true, force: true });
|
|
523
|
+
deleted.push(abs);
|
|
524
|
+
}
|
|
525
|
+
return deleted;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/lessons/import-legacy-merge.ts
|
|
529
|
+
async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
|
|
530
|
+
let addedLessons = 0;
|
|
531
|
+
const addedTriggers = /* @__PURE__ */ new Set();
|
|
532
|
+
const touchedTopics = /* @__PURE__ */ new Set();
|
|
533
|
+
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
534
|
+
addedLessons = 0;
|
|
535
|
+
addedTriggers.clear();
|
|
536
|
+
touchedTopics.clear();
|
|
537
|
+
for (const spec of specs) {
|
|
538
|
+
const result = addLessonInto(g, spec, {
|
|
539
|
+
allowNewTopic: true,
|
|
540
|
+
topicSummary: summaryByTopic.get(spec.topic),
|
|
541
|
+
// Legacy lessons may predate the ≥1-trigger requirement; recover them
|
|
542
|
+
// as-is rather than dropping historical knowledge.
|
|
543
|
+
allowNoTrigger: true
|
|
544
|
+
});
|
|
545
|
+
if (result.isNewLesson) addedLessons += 1;
|
|
546
|
+
for (const t of result.newTriggerIds) addedTriggers.add(t);
|
|
547
|
+
touchedTopics.add(spec.topic);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
551
|
+
return {
|
|
552
|
+
wroteGraphPath: paths.graph,
|
|
553
|
+
deletedPaths,
|
|
554
|
+
topicCount: touchedTopics.size,
|
|
555
|
+
lessonCount: addedLessons,
|
|
556
|
+
triggerCount: addedTriggers.size
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
var BASE_REL = ".agentsmesh/lessons";
|
|
560
|
+
function lessonsPaths(projectRoot) {
|
|
561
|
+
const base = join(projectRoot, BASE_REL);
|
|
562
|
+
return {
|
|
563
|
+
base,
|
|
564
|
+
graph: join(base, "lessons.json"),
|
|
565
|
+
config: join(base, "config.json"),
|
|
566
|
+
journal: join(base, "journal.md"),
|
|
567
|
+
index: join(base, "index.yaml"),
|
|
568
|
+
topicsDir: join(base, "topics")
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function toRelPath(projectRoot, absolute) {
|
|
572
|
+
return relative(projectRoot, absolute).split(sep).join("/");
|
|
573
|
+
}
|
|
574
|
+
var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
|
|
575
|
+
|
|
576
|
+
Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
|
|
577
|
+
|
|
578
|
+
**Recall \u2014 MUST run before every file edit and every state-changing command** (build/test/install/migrate/git-write): \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule. Pure-read commands (cat/ls/grep/git-log) and the recall query itself are exempt.
|
|
579
|
+
|
|
580
|
+
**Capture \u2014 MUST run immediately after any failure** (a failing test/CI/lint/typecheck, a code review, a user correction, a regression, or a wrong assumption \u2014 yours or anyone's): \`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
|
|
581
|
+
|
|
582
|
+
No shell? Use the \`lessons_query\` / \`lessons_add\` MCP tools. Skip either and the system does not exist.`;
|
|
583
|
+
|
|
584
|
+
// src/lessons/import-legacy.ts
|
|
585
|
+
var LessonsGraphExistsError = class extends Error {
|
|
586
|
+
code = "LESSONS_GRAPH_EXISTS";
|
|
587
|
+
constructor() {
|
|
588
|
+
super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
|
|
589
|
+
this.name = "LessonsGraphExistsError";
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
async function importLegacyLessons(projectRoot, options) {
|
|
593
|
+
const paths = lessonsPaths(projectRoot);
|
|
594
|
+
const indexRaw = readFileSync(paths.index, "utf8");
|
|
595
|
+
const index = LegacyIndexSchema.parse(parse(indexRaw));
|
|
596
|
+
const topics = {};
|
|
597
|
+
const triggersById = /* @__PURE__ */ new Map();
|
|
598
|
+
const triggerIdByKey = /* @__PURE__ */ new Map();
|
|
599
|
+
const lessons = {};
|
|
600
|
+
const specs = [];
|
|
601
|
+
const summaryByTopic = /* @__PURE__ */ new Map();
|
|
602
|
+
for (const cluster of index.clusters) {
|
|
603
|
+
topics[cluster.topic] = { summary: cluster.summary };
|
|
604
|
+
summaryByTopic.set(cluster.topic, cluster.summary);
|
|
605
|
+
const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
|
|
606
|
+
const topicFile = join(projectRoot, cluster.file);
|
|
607
|
+
if (!existsSync(topicFile)) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const topicMarkdown = readFileSync(topicFile, "utf8");
|
|
613
|
+
for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
|
|
614
|
+
const lessonEvidence = [
|
|
615
|
+
`legacy:${cluster.file}#rule-${ruleIndex}`,
|
|
616
|
+
...evidence.map((e) => `legacy:${e}`)
|
|
617
|
+
];
|
|
618
|
+
lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
|
|
619
|
+
rule: body,
|
|
620
|
+
topics: [cluster.topic],
|
|
621
|
+
triggers: clusterTriggerIds,
|
|
622
|
+
evidence: lessonEvidence,
|
|
623
|
+
status: "active",
|
|
624
|
+
createdAt: options.migratedAt
|
|
625
|
+
};
|
|
626
|
+
specs.push({
|
|
627
|
+
rule: body,
|
|
628
|
+
topic: cluster.topic,
|
|
629
|
+
triggers: {
|
|
630
|
+
files: cluster.triggers.file_globs,
|
|
631
|
+
commands: cluster.triggers.command_patterns,
|
|
632
|
+
keywords: cluster.triggers.keywords
|
|
633
|
+
},
|
|
634
|
+
evidence: lessonEvidence,
|
|
635
|
+
createdAt: options.migratedAt
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (options.merge === true)
|
|
640
|
+
return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
|
|
641
|
+
const triggers = Object.fromEntries(triggersById.entries());
|
|
642
|
+
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
643
|
+
const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
|
|
644
|
+
if (options.force !== true && populated) {
|
|
645
|
+
throw new LessonsGraphExistsError();
|
|
646
|
+
}
|
|
647
|
+
g.version = 1;
|
|
648
|
+
g.lessons = lessons;
|
|
649
|
+
g.topics = topics;
|
|
650
|
+
g.triggers = triggers;
|
|
651
|
+
});
|
|
652
|
+
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
653
|
+
return {
|
|
654
|
+
wroteGraphPath: paths.graph,
|
|
655
|
+
deletedPaths,
|
|
656
|
+
topicCount: Object.keys(topics).length,
|
|
657
|
+
lessonCount: Object.keys(lessons).length,
|
|
658
|
+
triggerCount: triggersById.size
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/lessons/auto-migrate.ts
|
|
663
|
+
function todayIso2() {
|
|
664
|
+
const now = /* @__PURE__ */ new Date();
|
|
665
|
+
const y = now.getUTCFullYear();
|
|
666
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
667
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
668
|
+
return `${y}-${m}-${d}`;
|
|
669
|
+
}
|
|
670
|
+
async function maybeAutoMigrateLessons(projectRoot) {
|
|
671
|
+
if (existsSync(graphFilePath(projectRoot))) return false;
|
|
672
|
+
const paths = lessonsPaths(projectRoot);
|
|
673
|
+
if (!existsSync(paths.index)) return false;
|
|
674
|
+
try {
|
|
675
|
+
await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
|
|
676
|
+
return true;
|
|
677
|
+
} catch (err) {
|
|
678
|
+
if (err instanceof LessonsGraphExistsError) return false;
|
|
679
|
+
throw err;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/core/errors.ts
|
|
684
|
+
var AgentsMeshError = class extends Error {
|
|
685
|
+
code;
|
|
686
|
+
constructor(code, message, options) {
|
|
687
|
+
super(message, options);
|
|
688
|
+
this.name = "AgentsMeshError";
|
|
689
|
+
this.code = code;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
var LockAcquisitionError = class extends AgentsMeshError {
|
|
693
|
+
lockPath;
|
|
694
|
+
holder;
|
|
695
|
+
/** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
|
|
696
|
+
label;
|
|
697
|
+
constructor(lockPath, holder, options) {
|
|
698
|
+
const label = options?.label ?? "lock";
|
|
699
|
+
super(
|
|
700
|
+
"AM_LOCK_ACQUISITION_FAILED",
|
|
701
|
+
`Could not acquire ${label} at ${lockPath}: currently held by ${holder}. Wait for the other process to finish, or remove ${lockPath} manually if you are sure no agentsmesh process is running.`,
|
|
702
|
+
options
|
|
703
|
+
);
|
|
704
|
+
this.name = "LockAcquisitionError";
|
|
705
|
+
this.lockPath = lockPath;
|
|
706
|
+
this.holder = holder;
|
|
707
|
+
this.label = label;
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
var FileSystemError = class extends AgentsMeshError {
|
|
711
|
+
path;
|
|
712
|
+
errnoCode;
|
|
713
|
+
constructor(path, message, options) {
|
|
714
|
+
super("AM_FILESYSTEM", message, options);
|
|
715
|
+
this.name = "FileSystemError";
|
|
716
|
+
this.path = path;
|
|
717
|
+
this.errnoCode = options?.errnoCode;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// src/utils/filesystem/process-lock.ts
|
|
722
|
+
var DEFAULT_STALE_MS = 6e4;
|
|
723
|
+
var DEFAULT_RETRIES = 30;
|
|
724
|
+
var DEFAULT_RETRY_DELAY_MS = 200;
|
|
725
|
+
var YOUNG_LOCK_GRACE_MS = 2e3;
|
|
726
|
+
async function acquireProcessLock(lockPath, opts = {}) {
|
|
727
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
728
|
+
const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
729
|
+
const stale = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
730
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
731
|
+
let attempt = 0;
|
|
732
|
+
while (true) {
|
|
733
|
+
const acquired = await tryAcquire(lockPath);
|
|
734
|
+
if (acquired) return acquired;
|
|
735
|
+
const existing = await inspectLock(lockPath);
|
|
736
|
+
if (existing !== "young" && isStale(existing, stale)) {
|
|
737
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (attempt >= retries) {
|
|
742
|
+
const holder = existing === "young" ? null : existing;
|
|
743
|
+
throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
|
|
744
|
+
}
|
|
745
|
+
attempt++;
|
|
746
|
+
await sleep(delay);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async function tryAcquire(lockPath) {
|
|
750
|
+
try {
|
|
751
|
+
await mkdir(lockPath, { recursive: false });
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err.code === "EEXIST") return null;
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
756
|
+
const metadataPath = join(lockPath, "holder.json");
|
|
757
|
+
const metadata = {
|
|
758
|
+
pid: process.pid,
|
|
759
|
+
started: Date.now(),
|
|
760
|
+
hostname: getHostname()
|
|
761
|
+
};
|
|
762
|
+
await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
763
|
+
let released = false;
|
|
764
|
+
const cleanup = () => {
|
|
765
|
+
if (released) return;
|
|
766
|
+
released = true;
|
|
767
|
+
try {
|
|
768
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const signalHandler = (signal) => {
|
|
773
|
+
cleanup();
|
|
774
|
+
process.kill(process.pid, signal);
|
|
775
|
+
};
|
|
776
|
+
process.once("SIGINT", signalHandler);
|
|
777
|
+
process.once("SIGTERM", signalHandler);
|
|
778
|
+
process.once("exit", cleanup);
|
|
779
|
+
return async () => {
|
|
780
|
+
if (released) return;
|
|
781
|
+
released = true;
|
|
782
|
+
process.off("SIGINT", signalHandler);
|
|
783
|
+
process.off("SIGTERM", signalHandler);
|
|
784
|
+
process.off("exit", cleanup);
|
|
785
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
786
|
+
});
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async function inspectLock(lockPath) {
|
|
790
|
+
try {
|
|
791
|
+
const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
|
|
792
|
+
const parsed = JSON.parse(raw);
|
|
793
|
+
if (!isLockMetadata(parsed)) return null;
|
|
794
|
+
return parsed;
|
|
795
|
+
} catch {
|
|
796
|
+
try {
|
|
797
|
+
const info = await stat(lockPath);
|
|
798
|
+
const ageMs = Date.now() - info.mtimeMs;
|
|
799
|
+
if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function isStale(meta, staleMs) {
|
|
806
|
+
if (!meta) return true;
|
|
807
|
+
const age = Date.now() - meta.started;
|
|
808
|
+
if (age > staleMs) return true;
|
|
809
|
+
if (meta.hostname && meta.hostname !== getHostname()) return false;
|
|
810
|
+
return !isProcessAlive(meta.pid);
|
|
811
|
+
}
|
|
812
|
+
function isProcessAlive(pid) {
|
|
813
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
814
|
+
try {
|
|
815
|
+
process.kill(pid, 0);
|
|
816
|
+
return true;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
return err.code === "EPERM";
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function describeHolder(meta) {
|
|
822
|
+
if (!meta) return "unknown (unreadable lock metadata)";
|
|
823
|
+
const host = meta.hostname ? `${meta.hostname}:` : "";
|
|
824
|
+
return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
|
|
825
|
+
}
|
|
826
|
+
function isLockMetadata(value) {
|
|
827
|
+
if (typeof value !== "object" || value === null) return false;
|
|
828
|
+
const v = value;
|
|
829
|
+
return typeof v.pid === "number" && typeof v.started === "number";
|
|
830
|
+
}
|
|
831
|
+
function getHostname() {
|
|
832
|
+
return hostname();
|
|
833
|
+
}
|
|
834
|
+
function sleep(ms) {
|
|
835
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/lessons/lessons-lock.ts
|
|
839
|
+
var LESSONS_LOCK_FILENAME = ".lessons.lock";
|
|
840
|
+
function lessonsLockPath(projectRoot) {
|
|
841
|
+
return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
|
|
842
|
+
}
|
|
843
|
+
async function acquireLessonsLock(projectRoot, opts = {}) {
|
|
844
|
+
const lockPath = lessonsLockPath(projectRoot);
|
|
845
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
846
|
+
return acquireProcessLock(lockPath, { ...opts, label: "lessons lock" });
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/lessons/validate-checks.ts
|
|
850
|
+
function collectDanglingRefs(graph, findings) {
|
|
851
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
852
|
+
for (const topicId of lesson.topics) {
|
|
853
|
+
if (graph.topics[topicId] === void 0) {
|
|
854
|
+
findings.push({
|
|
855
|
+
level: "error",
|
|
856
|
+
code: "DANGLING_TOPIC",
|
|
857
|
+
message: `Lesson "${lessonId}" references unknown topic "${topicId}".`,
|
|
858
|
+
lessonId,
|
|
859
|
+
topicId
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
for (const triggerId of lesson.triggers) {
|
|
864
|
+
if (graph.triggers[triggerId] === void 0) {
|
|
865
|
+
findings.push({
|
|
866
|
+
level: "error",
|
|
867
|
+
code: "DANGLING_TRIGGER",
|
|
868
|
+
message: `Lesson "${lessonId}" references unknown trigger "${triggerId}".`,
|
|
869
|
+
lessonId,
|
|
870
|
+
triggerId
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (lesson.supersededBy !== void 0 && graph.lessons[lesson.supersededBy] === void 0) {
|
|
875
|
+
findings.push({
|
|
876
|
+
level: "error",
|
|
877
|
+
code: "DANGLING_SUPERSEDER",
|
|
878
|
+
message: `Lesson "${lessonId}" supersededBy unknown lesson "${lesson.supersededBy}".`,
|
|
879
|
+
lessonId
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function collectDuplicateRefs(graph, findings) {
|
|
885
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
886
|
+
for (const topicId of firstDuplicates(lesson.topics)) {
|
|
887
|
+
findings.push({
|
|
888
|
+
level: "error",
|
|
889
|
+
code: "DUPLICATE_TOPIC_REF",
|
|
890
|
+
message: `Lesson "${lessonId}" references topic "${topicId}" more than once.`,
|
|
891
|
+
lessonId,
|
|
892
|
+
topicId
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
for (const triggerId of firstDuplicates(lesson.triggers)) {
|
|
896
|
+
findings.push({
|
|
897
|
+
level: "error",
|
|
898
|
+
code: "DUPLICATE_TRIGGER_REF",
|
|
899
|
+
message: `Lesson "${lessonId}" references trigger "${triggerId}" more than once.`,
|
|
900
|
+
lessonId,
|
|
901
|
+
triggerId
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function firstDuplicates(ids) {
|
|
907
|
+
const seen = /* @__PURE__ */ new Set();
|
|
908
|
+
const dup = /* @__PURE__ */ new Set();
|
|
909
|
+
for (const id of ids) {
|
|
910
|
+
if (seen.has(id)) dup.add(id);
|
|
911
|
+
else seen.add(id);
|
|
912
|
+
}
|
|
913
|
+
return [...dup];
|
|
914
|
+
}
|
|
915
|
+
function collectStatusInvariants(graph, findings) {
|
|
916
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
917
|
+
if (lesson.status === "superseded" && lesson.supersededBy === void 0) {
|
|
918
|
+
findings.push({
|
|
919
|
+
level: "error",
|
|
920
|
+
code: "SUPERSEDED_WITHOUT_TARGET",
|
|
921
|
+
message: `Lesson "${lessonId}" has status "superseded" but no supersededBy target.`,
|
|
922
|
+
lessonId
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (lesson.status === "active" && lesson.supersededBy !== void 0) {
|
|
926
|
+
findings.push({
|
|
927
|
+
level: "error",
|
|
928
|
+
code: "ACTIVE_WITH_SUPERSEDER",
|
|
929
|
+
message: `Lesson "${lessonId}" has status "active" but declares supersededBy.`,
|
|
930
|
+
lessonId
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function collectLifecycleInvariants(graph, findings) {
|
|
936
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
937
|
+
if (lesson.supersededBy === void 0) continue;
|
|
938
|
+
if (lesson.supersededBy === lessonId) {
|
|
939
|
+
findings.push({
|
|
940
|
+
level: "error",
|
|
941
|
+
code: "SELF_SUPERSEDED",
|
|
942
|
+
message: `Lesson "${lessonId}" is superseded by itself.`,
|
|
943
|
+
lessonId
|
|
944
|
+
});
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const target = graph.lessons[lesson.supersededBy];
|
|
948
|
+
if (target !== void 0 && target.status !== "active") {
|
|
949
|
+
findings.push({
|
|
950
|
+
level: "error",
|
|
951
|
+
code: "INACTIVE_SUPERSEDER",
|
|
952
|
+
message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target.status} \u2014 the chain dead-ends with no live replacement.`,
|
|
953
|
+
lessonId
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
collectSupersedeCycles(graph, findings);
|
|
958
|
+
}
|
|
959
|
+
function collectSupersedeCycles(graph, findings) {
|
|
960
|
+
const reported = /* @__PURE__ */ new Set();
|
|
961
|
+
for (const startId of Object.keys(graph.lessons)) {
|
|
962
|
+
const seen = /* @__PURE__ */ new Set();
|
|
963
|
+
let cur = startId;
|
|
964
|
+
while (cur !== void 0) {
|
|
965
|
+
if (seen.has(cur)) {
|
|
966
|
+
if (cur !== startId || reported.has(cur)) break;
|
|
967
|
+
reported.add(cur);
|
|
968
|
+
findings.push({
|
|
969
|
+
level: "error",
|
|
970
|
+
code: "SUPERSEDE_CYCLE",
|
|
971
|
+
message: `Lesson "${startId}" is part of a supersededBy cycle.`,
|
|
972
|
+
lessonId: startId
|
|
973
|
+
});
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
seen.add(cur);
|
|
977
|
+
const next = graph.lessons[cur]?.supersededBy;
|
|
978
|
+
if (next === cur) break;
|
|
979
|
+
cur = next;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function collectReachability(graph, findings) {
|
|
984
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
985
|
+
if (lesson.status === "active" && lesson.triggers.length === 0) {
|
|
986
|
+
findings.push({
|
|
987
|
+
level: "warning",
|
|
988
|
+
code: "UNREACHABLE_LESSON",
|
|
989
|
+
message: `Active lesson "${lessonId}" has no triggers and can never be recalled.`,
|
|
990
|
+
lessonId
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function collectOrphans(graph, findings) {
|
|
996
|
+
const referencedTopics = /* @__PURE__ */ new Set();
|
|
997
|
+
const referencedTriggers = /* @__PURE__ */ new Set();
|
|
998
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
999
|
+
for (const t of lesson.topics) referencedTopics.add(t);
|
|
1000
|
+
for (const t of lesson.triggers) referencedTriggers.add(t);
|
|
1001
|
+
}
|
|
1002
|
+
for (const topicId of Object.keys(graph.topics)) {
|
|
1003
|
+
if (!referencedTopics.has(topicId)) {
|
|
1004
|
+
findings.push({
|
|
1005
|
+
level: "warning",
|
|
1006
|
+
code: "ORPHAN_TOPIC",
|
|
1007
|
+
message: `Topic "${topicId}" is not referenced by any lesson.`,
|
|
1008
|
+
topicId
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
for (const triggerId of Object.keys(graph.triggers)) {
|
|
1013
|
+
if (!referencedTriggers.has(triggerId)) {
|
|
1014
|
+
findings.push({
|
|
1015
|
+
level: "warning",
|
|
1016
|
+
code: "ORPHAN_TRIGGER",
|
|
1017
|
+
message: `Trigger "${triggerId}" is not referenced by any lesson.`,
|
|
1018
|
+
triggerId
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/lessons/regex-linear/nfa-compile.ts
|
|
1025
|
+
var MAX_NFA_STATES = 2e3;
|
|
1026
|
+
var Builder = class {
|
|
1027
|
+
states = [];
|
|
1028
|
+
alloc() {
|
|
1029
|
+
if (this.states.length >= MAX_NFA_STATES) {
|
|
1030
|
+
throw new Error(`NFA state limit exceeded (${MAX_NFA_STATES}); pattern expands too large`);
|
|
1031
|
+
}
|
|
1032
|
+
this.states.push({ eps: [], asserts: [], chars: [] });
|
|
1033
|
+
return this.states.length - 1;
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
function isNonLineTerminator(c) {
|
|
1037
|
+
return c !== "\n" && c !== "\r" && c !== "\u2028" && c !== "\u2029";
|
|
1038
|
+
}
|
|
1039
|
+
function compileNode(b, node) {
|
|
1040
|
+
switch (node.k) {
|
|
1041
|
+
case "empty":
|
|
1042
|
+
case "assert": {
|
|
1043
|
+
const s = b.alloc();
|
|
1044
|
+
const e = b.alloc();
|
|
1045
|
+
if (node.k === "assert") b.states[s].asserts.push({ kind: node.kind, target: e });
|
|
1046
|
+
else b.states[s].eps.push(e);
|
|
1047
|
+
return { start: s, end: e };
|
|
1048
|
+
}
|
|
1049
|
+
case "char":
|
|
1050
|
+
case "any":
|
|
1051
|
+
case "class": {
|
|
1052
|
+
const s = b.alloc();
|
|
1053
|
+
const e = b.alloc();
|
|
1054
|
+
const test = node.k === "char" ? (c) => c === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
|
|
1055
|
+
b.states[s].chars.push({ test, target: e });
|
|
1056
|
+
return { start: s, end: e };
|
|
1057
|
+
}
|
|
1058
|
+
case "concat": {
|
|
1059
|
+
if (node.items.length === 0) return compileNode(b, { k: "empty" });
|
|
1060
|
+
let first = null;
|
|
1061
|
+
let prevEnd = -1;
|
|
1062
|
+
for (const item of node.items) {
|
|
1063
|
+
const frag = compileNode(b, item);
|
|
1064
|
+
if (first === null) first = frag;
|
|
1065
|
+
else b.states[prevEnd].eps.push(frag.start);
|
|
1066
|
+
prevEnd = frag.end;
|
|
1067
|
+
}
|
|
1068
|
+
return { start: first.start, end: prevEnd };
|
|
1069
|
+
}
|
|
1070
|
+
case "alt": {
|
|
1071
|
+
const s = b.alloc();
|
|
1072
|
+
const e = b.alloc();
|
|
1073
|
+
for (const opt of node.opts) {
|
|
1074
|
+
const frag = compileNode(b, opt);
|
|
1075
|
+
b.states[s].eps.push(frag.start);
|
|
1076
|
+
b.states[frag.end].eps.push(e);
|
|
1077
|
+
}
|
|
1078
|
+
return { start: s, end: e };
|
|
1079
|
+
}
|
|
1080
|
+
case "opt": {
|
|
1081
|
+
const s = b.alloc();
|
|
1082
|
+
const e = b.alloc();
|
|
1083
|
+
const frag = compileNode(b, node.node);
|
|
1084
|
+
b.states[s].eps.push(frag.start, e);
|
|
1085
|
+
b.states[frag.end].eps.push(e);
|
|
1086
|
+
return { start: s, end: e };
|
|
1087
|
+
}
|
|
1088
|
+
case "star": {
|
|
1089
|
+
const s = b.alloc();
|
|
1090
|
+
const e = b.alloc();
|
|
1091
|
+
const frag = compileNode(b, node.node);
|
|
1092
|
+
b.states[s].eps.push(frag.start, e);
|
|
1093
|
+
b.states[frag.end].eps.push(frag.start, e);
|
|
1094
|
+
return { start: s, end: e };
|
|
1095
|
+
}
|
|
1096
|
+
case "plus": {
|
|
1097
|
+
const e = b.alloc();
|
|
1098
|
+
const frag = compileNode(b, node.node);
|
|
1099
|
+
b.states[frag.end].eps.push(frag.start, e);
|
|
1100
|
+
return { start: frag.start, end: e };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function compileNfa(ast) {
|
|
1105
|
+
const b = new Builder();
|
|
1106
|
+
const { start, end } = compileNode(b, ast);
|
|
1107
|
+
return { states: b.states, start, accept: end };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/lessons/regex-linear/nfa.ts
|
|
1111
|
+
function wordBoundary(input, pos) {
|
|
1112
|
+
const before = pos > 0 && /[A-Za-z0-9_]/.test(input[pos - 1]);
|
|
1113
|
+
const after = pos < input.length && /[A-Za-z0-9_]/.test(input[pos]);
|
|
1114
|
+
return before !== after;
|
|
1115
|
+
}
|
|
1116
|
+
function assertHolds(kind, input, pos) {
|
|
1117
|
+
switch (kind) {
|
|
1118
|
+
case "start":
|
|
1119
|
+
return pos === 0;
|
|
1120
|
+
case "end":
|
|
1121
|
+
return pos === input.length;
|
|
1122
|
+
case "wordB":
|
|
1123
|
+
return wordBoundary(input, pos);
|
|
1124
|
+
case "nonWordB":
|
|
1125
|
+
return !wordBoundary(input, pos);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function buildMatcher(ast) {
|
|
1129
|
+
const { states, start, accept } = compileNfa(ast);
|
|
1130
|
+
const closure = (set, idx, input, pos, budget) => {
|
|
1131
|
+
const stack = [idx];
|
|
1132
|
+
while (stack.length > 0) {
|
|
1133
|
+
if (budget.remaining <= 0) return;
|
|
1134
|
+
const cur = stack.pop();
|
|
1135
|
+
if (set.has(cur)) continue;
|
|
1136
|
+
set.add(cur);
|
|
1137
|
+
budget.remaining -= 1;
|
|
1138
|
+
for (const t of states[cur].eps) if (!set.has(t)) stack.push(t);
|
|
1139
|
+
for (const a of states[cur].asserts) {
|
|
1140
|
+
if (assertHolds(a.kind, input, pos) && !set.has(a.target)) stack.push(a.target);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
return {
|
|
1145
|
+
// The input is matched in full (no truncation — truncation would miss suffix
|
|
1146
|
+
// matches and let `$` falsely match an invented endpoint). Work is bounded by
|
|
1147
|
+
// the shared budget instead: when it runs out we report a safe non-match.
|
|
1148
|
+
test(input, budget) {
|
|
1149
|
+
const b = budget ?? { remaining: Number.POSITIVE_INFINITY };
|
|
1150
|
+
if (b.remaining <= 0) return false;
|
|
1151
|
+
let current = /* @__PURE__ */ new Set();
|
|
1152
|
+
for (let pos = 0; pos <= input.length; pos += 1) {
|
|
1153
|
+
closure(current, start, input, pos, b);
|
|
1154
|
+
if (current.has(accept)) return true;
|
|
1155
|
+
if (b.remaining <= 0) return false;
|
|
1156
|
+
if (pos === input.length) break;
|
|
1157
|
+
const ch = input[pos];
|
|
1158
|
+
const next = /* @__PURE__ */ new Set();
|
|
1159
|
+
for (const s of current) {
|
|
1160
|
+
b.remaining -= 1;
|
|
1161
|
+
for (const t of states[s].chars) {
|
|
1162
|
+
if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
current = next;
|
|
1166
|
+
}
|
|
1167
|
+
return current.has(accept);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/lessons/regex-linear/ast.ts
|
|
1173
|
+
var UnsupportedRegexError = class extends Error {
|
|
1174
|
+
constructor(message) {
|
|
1175
|
+
super(message);
|
|
1176
|
+
this.name = "UnsupportedRegexError";
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// src/lessons/regex-linear/parse-helpers.ts
|
|
1181
|
+
var MAX_REPEAT = 1e3;
|
|
1182
|
+
var isWord = (c) => /[A-Za-z0-9_]/.test(c);
|
|
1183
|
+
function expandRepeat(atom, min, max) {
|
|
1184
|
+
const items = [];
|
|
1185
|
+
for (let k = 0; k < min; k += 1) items.push(atom);
|
|
1186
|
+
if (max === Infinity) {
|
|
1187
|
+
items.push({ k: "star", node: atom });
|
|
1188
|
+
} else {
|
|
1189
|
+
for (let k = min; k < max; k += 1) items.push({ k: "opt", node: atom });
|
|
1190
|
+
}
|
|
1191
|
+
if (items.length === 0) return { k: "empty" };
|
|
1192
|
+
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
1193
|
+
}
|
|
1194
|
+
function escapeClass(c) {
|
|
1195
|
+
switch (c) {
|
|
1196
|
+
case "d":
|
|
1197
|
+
return (x) => x >= "0" && x <= "9";
|
|
1198
|
+
case "D":
|
|
1199
|
+
return (x) => !(x >= "0" && x <= "9");
|
|
1200
|
+
case "w":
|
|
1201
|
+
return isWord;
|
|
1202
|
+
case "W":
|
|
1203
|
+
return (x) => !isWord(x);
|
|
1204
|
+
case "s":
|
|
1205
|
+
return (x) => /\s/.test(x);
|
|
1206
|
+
case "S":
|
|
1207
|
+
return (x) => !/\s/.test(x);
|
|
1208
|
+
default:
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
var HEX2 = /^[0-9a-fA-F]{2}$/;
|
|
1213
|
+
var HEX4 = /^[0-9a-fA-F]{4}$/;
|
|
1214
|
+
function readUnicodeEscape(src, i, c) {
|
|
1215
|
+
if (c === "x") {
|
|
1216
|
+
const hex2 = src.slice(i, i + 2);
|
|
1217
|
+
return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
|
|
1218
|
+
}
|
|
1219
|
+
const hex = src.slice(i, i + 4);
|
|
1220
|
+
return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
|
|
1221
|
+
}
|
|
1222
|
+
function readControlEscape(src, i) {
|
|
1223
|
+
const x = src[i];
|
|
1224
|
+
if (x === void 0 || !/[A-Za-z]/.test(x)) {
|
|
1225
|
+
throw new UnsupportedRegexError("\\c must be followed by a letter");
|
|
1226
|
+
}
|
|
1227
|
+
return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
|
|
1228
|
+
}
|
|
1229
|
+
function classEscapeChar(src, i, e) {
|
|
1230
|
+
if (e === "b") return { ch: "\b", len: 0 };
|
|
1231
|
+
if (e === "c") return readControlEscape(src, i);
|
|
1232
|
+
if (e === "x" || e === "u") return readUnicodeEscape(src, i, e);
|
|
1233
|
+
return { ch: escapeLiteral(e), len: 0 };
|
|
1234
|
+
}
|
|
1235
|
+
function escapeLiteral(c) {
|
|
1236
|
+
switch (c) {
|
|
1237
|
+
case "t":
|
|
1238
|
+
return " ";
|
|
1239
|
+
case "n":
|
|
1240
|
+
return "\n";
|
|
1241
|
+
case "r":
|
|
1242
|
+
return "\r";
|
|
1243
|
+
case "f":
|
|
1244
|
+
return "\f";
|
|
1245
|
+
case "v":
|
|
1246
|
+
return "\v";
|
|
1247
|
+
case "0":
|
|
1248
|
+
return "\0";
|
|
1249
|
+
default:
|
|
1250
|
+
return c;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/lessons/regex-linear/parse.ts
|
|
1255
|
+
function parseRegex(src) {
|
|
1256
|
+
let i = 0;
|
|
1257
|
+
const peek = () => src[i];
|
|
1258
|
+
const eat = () => src[i++];
|
|
1259
|
+
function parseAlt() {
|
|
1260
|
+
const opts = [parseConcat()];
|
|
1261
|
+
while (peek() === "|") {
|
|
1262
|
+
i += 1;
|
|
1263
|
+
opts.push(parseConcat());
|
|
1264
|
+
}
|
|
1265
|
+
return opts.length === 1 ? opts[0] : { k: "alt", opts };
|
|
1266
|
+
}
|
|
1267
|
+
function parseConcat() {
|
|
1268
|
+
const items = [];
|
|
1269
|
+
while (i < src.length && peek() !== "|" && peek() !== ")") {
|
|
1270
|
+
items.push(parseQuantified());
|
|
1271
|
+
}
|
|
1272
|
+
if (items.length === 0) return { k: "empty" };
|
|
1273
|
+
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
1274
|
+
}
|
|
1275
|
+
function parseQuantified() {
|
|
1276
|
+
const atom = parseAtom();
|
|
1277
|
+
const q = peek();
|
|
1278
|
+
if (q === "*" || q === "+" || q === "?") {
|
|
1279
|
+
i += 1;
|
|
1280
|
+
if (peek() === "?") i += 1;
|
|
1281
|
+
return q === "*" ? { k: "star", node: atom } : q === "+" ? { k: "plus", node: atom } : { k: "opt", node: atom };
|
|
1282
|
+
}
|
|
1283
|
+
if (q === "{") {
|
|
1284
|
+
const repeat = tryParseBrace();
|
|
1285
|
+
if (repeat !== null) return expandRepeat(atom, repeat.min, repeat.max);
|
|
1286
|
+
}
|
|
1287
|
+
return atom;
|
|
1288
|
+
}
|
|
1289
|
+
function tryParseBrace() {
|
|
1290
|
+
const m = /^\{(\d+)(,(\d*)?)?\}/.exec(src.slice(i));
|
|
1291
|
+
if (m === null) return null;
|
|
1292
|
+
i += m[0].length;
|
|
1293
|
+
if (peek() === "?") i += 1;
|
|
1294
|
+
const min = Number(m[1]);
|
|
1295
|
+
const max = m[2] === void 0 ? min : m[3] === "" || m[3] === void 0 ? Infinity : Number(m[3]);
|
|
1296
|
+
if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
|
|
1297
|
+
throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
|
|
1298
|
+
}
|
|
1299
|
+
return { min, max };
|
|
1300
|
+
}
|
|
1301
|
+
function parseAtom() {
|
|
1302
|
+
const c = peek();
|
|
1303
|
+
if (c === "(") return parseGroup();
|
|
1304
|
+
if (c === "[") return parseClass();
|
|
1305
|
+
if (c === "\\") return parseEscape();
|
|
1306
|
+
if (c === ".") {
|
|
1307
|
+
i += 1;
|
|
1308
|
+
return { k: "any" };
|
|
1309
|
+
}
|
|
1310
|
+
if (c === "^") {
|
|
1311
|
+
i += 1;
|
|
1312
|
+
return { k: "assert", kind: "start" };
|
|
1313
|
+
}
|
|
1314
|
+
if (c === "$") {
|
|
1315
|
+
i += 1;
|
|
1316
|
+
return { k: "assert", kind: "end" };
|
|
1317
|
+
}
|
|
1318
|
+
if (c === void 0 || c === "*" || c === "+" || c === "?" || c === ")") {
|
|
1319
|
+
throw new UnsupportedRegexError(`Unexpected '${c ?? "<end>"}' in pattern`);
|
|
1320
|
+
}
|
|
1321
|
+
i += 1;
|
|
1322
|
+
return { k: "char", ch: c };
|
|
1323
|
+
}
|
|
1324
|
+
function parseGroup() {
|
|
1325
|
+
i += 1;
|
|
1326
|
+
if (peek() === "?") {
|
|
1327
|
+
const c2 = src[i + 1];
|
|
1328
|
+
if (c2 === "=" || c2 === "!" || c2 === "<") {
|
|
1329
|
+
if (!(c2 === "<" && /[A-Za-z]/.test(src[i + 2] ?? ""))) {
|
|
1330
|
+
throw new UnsupportedRegexError("Lookaround assertions are not supported");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (c2 === ":") i += 2;
|
|
1334
|
+
else if (c2 === "<") {
|
|
1335
|
+
i += 2;
|
|
1336
|
+
while (i < src.length && src[i] !== ">") i += 1;
|
|
1337
|
+
i += 1;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const inner = parseAlt();
|
|
1341
|
+
if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
|
|
1342
|
+
i += 1;
|
|
1343
|
+
return inner;
|
|
1344
|
+
}
|
|
1345
|
+
function parseEscape() {
|
|
1346
|
+
i += 1;
|
|
1347
|
+
const c = peek();
|
|
1348
|
+
if (c === void 0) throw new UnsupportedRegexError("Trailing backslash");
|
|
1349
|
+
if (/[1-9]/.test(c) || c === "k")
|
|
1350
|
+
throw new UnsupportedRegexError("Backreferences are not supported");
|
|
1351
|
+
i += 1;
|
|
1352
|
+
if (c === "b") return { k: "assert", kind: "wordB" };
|
|
1353
|
+
if (c === "B") return { k: "assert", kind: "nonWordB" };
|
|
1354
|
+
const cls = escapeClass(c);
|
|
1355
|
+
if (cls !== null) return { k: "class", test: cls };
|
|
1356
|
+
if (c === "x" || c === "u" || c === "c") {
|
|
1357
|
+
const { ch, len } = c === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c);
|
|
1358
|
+
i += len;
|
|
1359
|
+
return { k: "char", ch };
|
|
1360
|
+
}
|
|
1361
|
+
return { k: "char", ch: escapeLiteral(c) };
|
|
1362
|
+
}
|
|
1363
|
+
function parseClass() {
|
|
1364
|
+
i += 1;
|
|
1365
|
+
const negate = peek() === "^";
|
|
1366
|
+
if (negate) i += 1;
|
|
1367
|
+
const tests = [];
|
|
1368
|
+
while (i < src.length && peek() !== "]") {
|
|
1369
|
+
tests.push(parseClassMember());
|
|
1370
|
+
}
|
|
1371
|
+
if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
|
|
1372
|
+
i += 1;
|
|
1373
|
+
const base = (c) => tests.some((t) => t(c));
|
|
1374
|
+
return { k: "class", test: negate ? (c) => !base(c) : base };
|
|
1375
|
+
}
|
|
1376
|
+
function parseClassMember() {
|
|
1377
|
+
let lo;
|
|
1378
|
+
if (peek() === "\\") {
|
|
1379
|
+
i += 1;
|
|
1380
|
+
const e = eat();
|
|
1381
|
+
const cls = escapeClass(e);
|
|
1382
|
+
if (cls !== null) return cls;
|
|
1383
|
+
const r = classEscapeChar(src, i, e);
|
|
1384
|
+
i += r.len;
|
|
1385
|
+
lo = r.ch;
|
|
1386
|
+
} else {
|
|
1387
|
+
lo = eat();
|
|
1388
|
+
}
|
|
1389
|
+
if (peek() === "-" && src[i + 1] !== void 0 && src[i + 1] !== "]") {
|
|
1390
|
+
i += 1;
|
|
1391
|
+
let hi;
|
|
1392
|
+
if (peek() === "\\") {
|
|
1393
|
+
i += 1;
|
|
1394
|
+
const e2 = eat();
|
|
1395
|
+
const r = classEscapeChar(src, i, e2);
|
|
1396
|
+
i += r.len;
|
|
1397
|
+
hi = r.ch;
|
|
1398
|
+
} else {
|
|
1399
|
+
hi = eat();
|
|
1400
|
+
}
|
|
1401
|
+
const a = lo.codePointAt(0);
|
|
1402
|
+
const b = hi.codePointAt(0);
|
|
1403
|
+
return (c) => {
|
|
1404
|
+
const p = c.codePointAt(0);
|
|
1405
|
+
return p >= a && p <= b;
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
return (c) => c === lo;
|
|
1409
|
+
}
|
|
1410
|
+
const ast = parseAlt();
|
|
1411
|
+
if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
|
|
1412
|
+
return ast;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/lessons/regex-linear/index.ts
|
|
1416
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1417
|
+
function compileLinearMatcher(pattern) {
|
|
1418
|
+
const hit = cache.get(pattern);
|
|
1419
|
+
if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
|
|
1420
|
+
let matcher;
|
|
1421
|
+
try {
|
|
1422
|
+
matcher = buildMatcher(parseRegex(pattern));
|
|
1423
|
+
} catch {
|
|
1424
|
+
matcher = null;
|
|
1425
|
+
}
|
|
1426
|
+
cache.set(pattern, matcher);
|
|
1427
|
+
return matcher;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// src/lessons/regex-safety.ts
|
|
1431
|
+
var MAX_PATTERN_LENGTH = 1e3;
|
|
1432
|
+
function isSafeRegexPattern(pattern) {
|
|
1433
|
+
if (pattern.length > MAX_PATTERN_LENGTH) return false;
|
|
1434
|
+
return compileLinearMatcher(pattern) !== null;
|
|
1435
|
+
}
|
|
1436
|
+
function getCommandMatcher(pattern) {
|
|
1437
|
+
if (pattern.length > MAX_PATTERN_LENGTH) return null;
|
|
1438
|
+
return compileLinearMatcher(pattern);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/lessons/validate-quality.ts
|
|
1442
|
+
function collectDuplicateRules(graph, findings) {
|
|
1443
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1444
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1445
|
+
if (lesson.status !== "active") continue;
|
|
1446
|
+
const key = normalizeRule2(lesson.rule);
|
|
1447
|
+
const bucket = byKey.get(key) ?? [];
|
|
1448
|
+
bucket.push(lessonId);
|
|
1449
|
+
byKey.set(key, bucket);
|
|
1450
|
+
}
|
|
1451
|
+
for (const [key, ids] of byKey) {
|
|
1452
|
+
if (ids.length < 2) continue;
|
|
1453
|
+
const sorted = [...ids].sort();
|
|
1454
|
+
for (const lessonId of sorted) {
|
|
1455
|
+
const others = sorted.filter((other) => other !== lessonId);
|
|
1456
|
+
findings.push({
|
|
1457
|
+
level: "error",
|
|
1458
|
+
code: "DUPLICATE_RULE",
|
|
1459
|
+
message: `Lesson "${lessonId}" duplicates rule text of: ${others.join(", ")} (normalized key: "${key.slice(0, 60)}").`,
|
|
1460
|
+
lessonId
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function collectInvalidTriggerPatterns(graph, findings) {
|
|
1466
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1467
|
+
if (trigger.kind !== "command_pattern") continue;
|
|
1468
|
+
try {
|
|
1469
|
+
new RegExp(trigger.pattern);
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
findings.push({
|
|
1472
|
+
level: "error",
|
|
1473
|
+
code: "INVALID_TRIGGER_PATTERN",
|
|
1474
|
+
message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${err instanceof Error ? err.message : String(err)}.`,
|
|
1475
|
+
triggerId
|
|
1476
|
+
});
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (!isSafeRegexPattern(trigger.pattern)) {
|
|
1480
|
+
findings.push({
|
|
1481
|
+
level: "error",
|
|
1482
|
+
code: "UNSAFE_TRIGGER_PATTERN",
|
|
1483
|
+
message: `Trigger "${triggerId}" has a command_pattern regex outside the provably-linear subset (${trigger.pattern}): it can backtrack catastrophically (e.g. a quantified group like (a+)+ or (a|aa)+, adjacent repetition like a+a+, or a backreference/lookaround). Rewrite using a linear pattern.`,
|
|
1484
|
+
triggerId
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function collectBackslashGlobPatterns(graph, findings) {
|
|
1490
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1491
|
+
if (trigger.kind !== "file_glob") continue;
|
|
1492
|
+
if (!trigger.pattern.includes("\\")) continue;
|
|
1493
|
+
findings.push({
|
|
1494
|
+
level: "error",
|
|
1495
|
+
code: "BACKSLASH_GLOB_PATTERN",
|
|
1496
|
+
message: `Trigger "${triggerId}" has a file_glob pattern with a backslash (${trigger.pattern}); recall normalizes paths to forward slashes, so it never fires. Replace \\ with /.`,
|
|
1497
|
+
triggerId
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function collectDuplicateTriggers(graph, findings) {
|
|
1502
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1503
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1504
|
+
const key = `${trigger.kind}|${trigger.pattern}`;
|
|
1505
|
+
const bucket = byKey.get(key) ?? [];
|
|
1506
|
+
bucket.push(triggerId);
|
|
1507
|
+
byKey.set(key, bucket);
|
|
1508
|
+
}
|
|
1509
|
+
for (const [key, ids] of byKey) {
|
|
1510
|
+
if (ids.length < 2) continue;
|
|
1511
|
+
const sorted = [...ids].sort();
|
|
1512
|
+
for (const triggerId of sorted) {
|
|
1513
|
+
const others = sorted.filter((other) => other !== triggerId);
|
|
1514
|
+
findings.push({
|
|
1515
|
+
level: "error",
|
|
1516
|
+
code: "DUPLICATE_TRIGGER",
|
|
1517
|
+
message: `Trigger "${triggerId}" duplicates (kind, pattern) of: ${others.join(", ")} (key: "${key}").`,
|
|
1518
|
+
triggerId
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
var HIGH_FANOUT_THRESHOLD = 10;
|
|
1524
|
+
function collectFanout(graph, findings) {
|
|
1525
|
+
const fanout = /* @__PURE__ */ new Map();
|
|
1526
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1527
|
+
if (lesson.status !== "active") continue;
|
|
1528
|
+
for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
|
|
1529
|
+
}
|
|
1530
|
+
let over = 0;
|
|
1531
|
+
let max = 0;
|
|
1532
|
+
for (const n of fanout.values()) {
|
|
1533
|
+
if (n > HIGH_FANOUT_THRESHOLD) over += 1;
|
|
1534
|
+
if (n > max) max = n;
|
|
1535
|
+
}
|
|
1536
|
+
if (over > 0) {
|
|
1537
|
+
findings.push({
|
|
1538
|
+
level: "warning",
|
|
1539
|
+
code: "HIGH_FANOUT_TRIGGERS",
|
|
1540
|
+
message: `${over} trigger(s) each match more than ${HIGH_FANOUT_THRESHOLD} active lessons (max ${max}); recall returns the ranked top by default \u2014 consider per-lesson trigger refinement to improve precision.`
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function collectLowSignalKeywords(graph, findings) {
|
|
1545
|
+
const activeTriggerIds2 = /* @__PURE__ */ new Set();
|
|
1546
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1547
|
+
if (lesson.status !== "active") continue;
|
|
1548
|
+
for (const t of lesson.triggers) activeTriggerIds2.add(t);
|
|
1549
|
+
}
|
|
1550
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1551
|
+
if (trigger.kind !== "keyword") continue;
|
|
1552
|
+
if (!activeTriggerIds2.has(triggerId)) continue;
|
|
1553
|
+
if (!isLowSignalKeyword(trigger.pattern)) continue;
|
|
1554
|
+
findings.push({
|
|
1555
|
+
level: "warning",
|
|
1556
|
+
code: "LOW_SIGNAL_KEYWORD",
|
|
1557
|
+
message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a substring of --keyword or a contiguous token-run in the file/command, so it rarely fires \u2014 use a short distinctive phrase.`,
|
|
1558
|
+
triggerId
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
function collectStopwordKeywords(graph, findings) {
|
|
1563
|
+
const activeTriggerIds2 = /* @__PURE__ */ new Set();
|
|
1564
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1565
|
+
if (lesson.status !== "active") continue;
|
|
1566
|
+
for (const t of lesson.triggers) activeTriggerIds2.add(t);
|
|
1567
|
+
}
|
|
1568
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1569
|
+
if (trigger.kind !== "keyword") continue;
|
|
1570
|
+
if (!activeTriggerIds2.has(triggerId)) continue;
|
|
1571
|
+
if (tokenize(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
findings.push({
|
|
1575
|
+
level: "warning",
|
|
1576
|
+
code: "STOPWORD_KEYWORD",
|
|
1577
|
+
message: `Keyword trigger "${triggerId}" (${trigger.pattern}) loses tokens to stopword filtering, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path \u2014 drop the stopwords (e.g. "state art" instead of "state of the art"), or detach it with \`lessons untrigger\`.`,
|
|
1578
|
+
triggerId
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function normalizeRule2(rule) {
|
|
1583
|
+
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/lessons/validate.ts
|
|
1587
|
+
function validateLessonsGraph(graph, options = {}) {
|
|
1588
|
+
const findings = [];
|
|
1589
|
+
const schemaResult = LessonsGraphSchema.safeParse(graph);
|
|
1590
|
+
if (!schemaResult.success) {
|
|
1591
|
+
findings.push({
|
|
1592
|
+
level: "error",
|
|
1593
|
+
code: "SCHEMA_INVALID",
|
|
1594
|
+
message: schemaResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
1595
|
+
});
|
|
1596
|
+
return { ok: false, findings };
|
|
1597
|
+
}
|
|
1598
|
+
collectDanglingRefs(graph, findings);
|
|
1599
|
+
collectDuplicateRefs(graph, findings);
|
|
1600
|
+
collectStatusInvariants(graph, findings);
|
|
1601
|
+
collectLifecycleInvariants(graph, findings);
|
|
1602
|
+
collectDuplicateRules(graph, findings);
|
|
1603
|
+
collectReachability(graph, findings);
|
|
1604
|
+
collectInvalidTriggerPatterns(graph, findings);
|
|
1605
|
+
collectBackslashGlobPatterns(graph, findings);
|
|
1606
|
+
collectDuplicateTriggers(graph, findings);
|
|
1607
|
+
collectOrphans(graph, findings);
|
|
1608
|
+
collectFanout(graph, findings);
|
|
1609
|
+
collectLowSignalKeywords(graph, findings);
|
|
1610
|
+
collectStopwordKeywords(graph, findings);
|
|
1611
|
+
collectRunnerAnchoredPatterns(graph, findings);
|
|
1612
|
+
if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
|
|
1613
|
+
const ok = findings.every((f) => f.level !== "error");
|
|
1614
|
+
return { ok, findings };
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/lessons/mutate.ts
|
|
1618
|
+
function emptyGraph() {
|
|
1619
|
+
return { version: 1, lessons: {}, topics: {}, triggers: {} };
|
|
1620
|
+
}
|
|
1621
|
+
async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
|
|
1622
|
+
const release = await acquireLessonsLock(projectRoot, { retries: options.retries });
|
|
1623
|
+
try {
|
|
1624
|
+
const graph = tryLoadLessonsGraph(projectRoot) ?? emptyGraph();
|
|
1625
|
+
const result = await mutator(graph);
|
|
1626
|
+
const report = validateLessonsGraph(graph);
|
|
1627
|
+
if (!report.ok) {
|
|
1628
|
+
const errors = report.findings.filter((f) => f.level === "error").map((f) => `${f.code}: ${f.message}`).join("; ");
|
|
1629
|
+
throw new Error(`mutateLessonsGraph: refusing to write an invalid graph \u2014 ${errors}`);
|
|
1630
|
+
}
|
|
1631
|
+
saveLessonsGraph(projectRoot, graph);
|
|
1632
|
+
return result;
|
|
1633
|
+
} finally {
|
|
1634
|
+
await release();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
|
|
1638
|
+
await maybeAutoMigrateLessons(projectRoot);
|
|
1639
|
+
return mutateLessonsGraphLocked(projectRoot, mutator, options);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/lessons/trigger-effectiveness.ts
|
|
1643
|
+
function ineffectiveTriggers(graph, triggerIds) {
|
|
1644
|
+
const out = [];
|
|
1645
|
+
for (const id of triggerIds) {
|
|
1646
|
+
const trigger = graph.triggers[id];
|
|
1647
|
+
if (trigger === void 0) continue;
|
|
1648
|
+
const reason = ineffectiveReason(trigger.kind, trigger.pattern);
|
|
1649
|
+
if (reason !== null) out.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
|
|
1650
|
+
}
|
|
1651
|
+
return out;
|
|
1652
|
+
}
|
|
1653
|
+
function ineffectiveReason(kind, pattern) {
|
|
1654
|
+
if (kind === "keyword") {
|
|
1655
|
+
if (tokenize(pattern).length === 0) {
|
|
1656
|
+
return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
|
|
1657
|
+
}
|
|
1658
|
+
if (keywordNeedleLosesTokens(pattern)) {
|
|
1659
|
+
return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
|
|
1660
|
+
}
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
if (kind === "command_pattern") {
|
|
1664
|
+
let valid = true;
|
|
1665
|
+
try {
|
|
1666
|
+
new RegExp(pattern);
|
|
1667
|
+
} catch {
|
|
1668
|
+
valid = false;
|
|
1669
|
+
}
|
|
1670
|
+
if (!valid) {
|
|
1671
|
+
return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
|
|
1672
|
+
}
|
|
1673
|
+
if (!isSafeRegexPattern(pattern)) {
|
|
1674
|
+
return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
|
|
1675
|
+
}
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
function blockingDeadTriggers(graph, triggerIds) {
|
|
1681
|
+
return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/lessons/add.ts
|
|
1685
|
+
function countInputTriggers(triggers) {
|
|
1686
|
+
return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
|
|
1687
|
+
}
|
|
1688
|
+
async function addLesson(projectRoot, input, options = {}) {
|
|
1689
|
+
return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
|
|
1690
|
+
retries: options.retries
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
function addLessonInto(graph, input, options) {
|
|
1694
|
+
const ruleKey = normalizeRule(input.rule);
|
|
1695
|
+
const trimmedRule = input.rule.trim();
|
|
1696
|
+
if (trimmedRule.length > MAX_RULE_LENGTH) {
|
|
1697
|
+
throw new RuleTooLongError(trimmedRule.length, MAX_RULE_LENGTH);
|
|
1698
|
+
}
|
|
1699
|
+
const existingId = findExistingLessonByRule(graph, ruleKey);
|
|
1700
|
+
const isNewTopic = graph.topics[input.topic] === void 0;
|
|
1701
|
+
if (isNewTopic) {
|
|
1702
|
+
if (options.allowNewTopic !== true) throw new UnknownTopicError(input.topic);
|
|
1703
|
+
if (options.topicSummary === void 0 || options.topicSummary.length === 0) {
|
|
1704
|
+
throw new Error(`addLesson: new topic "${input.topic}" requires topicSummary.`);
|
|
1705
|
+
}
|
|
1706
|
+
graph.topics[input.topic] = { summary: options.topicSummary };
|
|
1707
|
+
}
|
|
1708
|
+
if (options.allowNoTrigger !== true) {
|
|
1709
|
+
const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
|
|
1710
|
+
if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
|
|
1711
|
+
throw new NoTriggerError();
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
|
|
1715
|
+
if (options.allowNoTrigger !== true) {
|
|
1716
|
+
const resultingTriggers = existingId !== null ? union(graph.lessons[existingId].triggers, triggerIds) : triggerIds;
|
|
1717
|
+
const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
|
|
1718
|
+
if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
|
|
1719
|
+
throw new UnrecallableLessonError(blockingDead);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (existingId !== null) {
|
|
1723
|
+
const existing = graph.lessons[existingId];
|
|
1724
|
+
graph.lessons[existingId] = {
|
|
1725
|
+
...existing,
|
|
1726
|
+
topics: union(existing.topics, [input.topic]),
|
|
1727
|
+
triggers: union(existing.triggers, triggerIds),
|
|
1728
|
+
evidence: union(existing.evidence, input.evidence ?? []),
|
|
1729
|
+
...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {}
|
|
1730
|
+
};
|
|
1731
|
+
return {
|
|
1732
|
+
id: existingId,
|
|
1733
|
+
isNewLesson: false,
|
|
1734
|
+
isNewTopic,
|
|
1735
|
+
newTriggerIds,
|
|
1736
|
+
// Near-duplicate detection is meaningless on an upsert (the lesson IS the
|
|
1737
|
+
// match), so only DEAD_GLOB/hygiene warnings apply here.
|
|
1738
|
+
warnings: inspectCapturedLesson(graph, existingId, options.knownPaths)
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
const id = makeLessonId(graph, input.topic, ruleKey);
|
|
1742
|
+
graph.lessons[id] = {
|
|
1743
|
+
rule: trimmedRule,
|
|
1744
|
+
topics: [input.topic],
|
|
1745
|
+
triggers: triggerIds,
|
|
1746
|
+
evidence: input.evidence === void 0 ? [] : [...input.evidence],
|
|
1747
|
+
status: "active",
|
|
1748
|
+
createdAt: input.createdAt ?? todayIso(),
|
|
1749
|
+
...input.rationale === void 0 ? {} : { rationale: input.rationale }
|
|
1750
|
+
};
|
|
1751
|
+
const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
|
|
1752
|
+
const nearDup = nearDuplicateWarning(graph, id);
|
|
1753
|
+
return {
|
|
1754
|
+
id,
|
|
1755
|
+
isNewLesson: true,
|
|
1756
|
+
isNewTopic,
|
|
1757
|
+
newTriggerIds,
|
|
1758
|
+
warnings: nearDup === null ? warnings : [...warnings, nearDup]
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function findExistingLessonByRule(graph, ruleKey) {
|
|
1762
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1763
|
+
if (lesson.status !== "active") continue;
|
|
1764
|
+
if (normalizeRule(lesson.rule) === ruleKey) return id;
|
|
1765
|
+
}
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/lessons/ranking-signals.ts
|
|
1770
|
+
function buildFanout(graph) {
|
|
1771
|
+
const fanout = /* @__PURE__ */ new Map();
|
|
1772
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1773
|
+
if (lesson.status !== "active") continue;
|
|
1774
|
+
for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
|
|
1775
|
+
}
|
|
1776
|
+
return fanout;
|
|
1777
|
+
}
|
|
1778
|
+
function buildTopicCoherence(matches) {
|
|
1779
|
+
const topicCount = /* @__PURE__ */ new Map();
|
|
1780
|
+
for (const { lesson } of matches) {
|
|
1781
|
+
for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
|
|
1782
|
+
}
|
|
1783
|
+
const coherence = /* @__PURE__ */ new Map();
|
|
1784
|
+
for (const { id, lesson } of matches) {
|
|
1785
|
+
let best = 0;
|
|
1786
|
+
for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
|
|
1787
|
+
coherence.set(id, best);
|
|
1788
|
+
}
|
|
1789
|
+
return coherence;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// src/lessons/prune.ts
|
|
1793
|
+
function planPrune(graph, options = {}) {
|
|
1794
|
+
const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
|
|
1795
|
+
const fanout = buildFanout(graph);
|
|
1796
|
+
const trimmedLessons = [];
|
|
1797
|
+
const keptByLesson = /* @__PURE__ */ new Map();
|
|
1798
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1799
|
+
if (lesson.status !== "active") continue;
|
|
1800
|
+
if (options.trimOverCap === false || lesson.triggers.length <= cap) {
|
|
1801
|
+
keptByLesson.set(id, lesson.triggers);
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
const ordered = [...lesson.triggers].sort((a, b) => {
|
|
1805
|
+
const fa = fanout.get(a);
|
|
1806
|
+
const fb = fanout.get(b);
|
|
1807
|
+
return fa !== fb ? fa - fb : a < b ? -1 : 1;
|
|
1808
|
+
});
|
|
1809
|
+
const drop = new Set(ordered.slice(cap));
|
|
1810
|
+
const kept = lesson.triggers.filter((t) => !drop.has(t));
|
|
1811
|
+
keptByLesson.set(id, kept);
|
|
1812
|
+
trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
|
|
1813
|
+
}
|
|
1814
|
+
const removedDeadGlobs = [];
|
|
1815
|
+
const unreachableLessons = [];
|
|
1816
|
+
if (options.knownPaths !== void 0) {
|
|
1817
|
+
const dead = deadFileGlobIds(graph, options.knownPaths);
|
|
1818
|
+
if (dead.size > 0) {
|
|
1819
|
+
for (const [id, kept] of keptByLesson) {
|
|
1820
|
+
const deadInLesson = kept.filter((t) => dead.has(t));
|
|
1821
|
+
if (deadInLesson.length === 0) continue;
|
|
1822
|
+
const remaining = kept.filter((t) => !dead.has(t));
|
|
1823
|
+
if (remaining.length >= 1) {
|
|
1824
|
+
keptByLesson.set(id, remaining);
|
|
1825
|
+
removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
|
|
1826
|
+
} else {
|
|
1827
|
+
unreachableLessons.push(id);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
unreachableLessons.sort();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
const live = /* @__PURE__ */ new Set();
|
|
1834
|
+
for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
|
|
1835
|
+
const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
|
|
1836
|
+
const referencedTopics = /* @__PURE__ */ new Set();
|
|
1837
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1838
|
+
for (const topic of lesson.topics) referencedTopics.add(topic);
|
|
1839
|
+
}
|
|
1840
|
+
const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
|
|
1841
|
+
return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
|
|
1842
|
+
}
|
|
1843
|
+
function applyPruneToGraph(graph, plan) {
|
|
1844
|
+
for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
|
|
1845
|
+
const lesson = graph.lessons[trim.id];
|
|
1846
|
+
if (lesson === void 0) continue;
|
|
1847
|
+
const drop = new Set(trim.removedTriggers);
|
|
1848
|
+
graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
|
|
1849
|
+
}
|
|
1850
|
+
for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
|
|
1851
|
+
const dead = new Set(plan.removedTriggerIds);
|
|
1852
|
+
if (dead.size === 0) return;
|
|
1853
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1854
|
+
if (lesson.triggers.some((t) => dead.has(t))) {
|
|
1855
|
+
graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
for (const t of dead) delete graph.triggers[t];
|
|
1859
|
+
}
|
|
1860
|
+
function isEmptyPrunePlan(plan) {
|
|
1861
|
+
return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/lessons/auto-prune.ts
|
|
1865
|
+
function isAutoPruneEnabled(projectRoot) {
|
|
1866
|
+
const path = lessonsPaths(projectRoot).config;
|
|
1867
|
+
if (!existsSync(path)) return false;
|
|
1868
|
+
try {
|
|
1869
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1870
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
1871
|
+
return parsed.autoPrune === true;
|
|
1872
|
+
} catch {
|
|
1873
|
+
return false;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function maybeAutoPrune(projectRoot, knownPaths) {
|
|
1877
|
+
if (!isAutoPruneEnabled(projectRoot)) return null;
|
|
1878
|
+
const preview = tryLoadLessonsGraph(projectRoot);
|
|
1879
|
+
if (preview === null) return null;
|
|
1880
|
+
if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
|
|
1881
|
+
let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
|
|
1882
|
+
await mutateLessonsGraph(projectRoot, (graph) => {
|
|
1883
|
+
const plan = planPrune(graph, { trimOverCap: false, knownPaths });
|
|
1884
|
+
if (isEmptyPrunePlan(plan)) return;
|
|
1885
|
+
applyPruneToGraph(graph, plan);
|
|
1886
|
+
summary = {
|
|
1887
|
+
removedTriggers: plan.removedTriggerIds.length,
|
|
1888
|
+
removedTopics: plan.removedTopicIds.length,
|
|
1889
|
+
detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
|
|
1890
|
+
};
|
|
1891
|
+
});
|
|
1892
|
+
const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
|
|
1893
|
+
return total > 0 ? summary : null;
|
|
1894
|
+
}
|
|
1895
|
+
function appendJsonl(path, record, opts) {
|
|
1896
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1897
|
+
appendFileSync(path, `${JSON.stringify(record)}
|
|
1898
|
+
`, "utf8");
|
|
1899
|
+
if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
|
|
1900
|
+
}
|
|
1901
|
+
function capJsonl(path, maxRecords) {
|
|
1902
|
+
if (!existsSync(path)) return;
|
|
1903
|
+
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
1904
|
+
if (lines.length <= maxRecords) return;
|
|
1905
|
+
const kept = lines.slice(lines.length - maxRecords);
|
|
1906
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
1907
|
+
writeFileSync(tmp, `${kept.join("\n")}
|
|
1908
|
+
`, "utf8");
|
|
1909
|
+
renameSync(tmp, path);
|
|
1910
|
+
}
|
|
1911
|
+
var MAX_RECALL_LOG_RECORDS = 5e3;
|
|
1912
|
+
var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
1913
|
+
var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
|
|
1914
|
+
var SESSION_ENV = "AGENTSMESH_SESSION_ID";
|
|
1915
|
+
function sessionId(env = process.env) {
|
|
1916
|
+
const raw = env[SESSION_ENV];
|
|
1917
|
+
return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
|
|
1918
|
+
}
|
|
1919
|
+
function recallLogPath(projectRoot) {
|
|
1920
|
+
return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
|
|
1921
|
+
}
|
|
1922
|
+
function isTelemetryEnabled(env = process.env) {
|
|
1923
|
+
return env[TELEMETRY_ENV] === "1";
|
|
1924
|
+
}
|
|
1925
|
+
function appendRecallRecord(projectRoot, record, env = process.env) {
|
|
1926
|
+
if (!isTelemetryEnabled(env)) return;
|
|
1927
|
+
appendJsonl(recallLogPath(projectRoot), record, {
|
|
1928
|
+
maxRecords: MAX_RECALL_LOG_RECORDS,
|
|
1929
|
+
trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/lessons/capture-telemetry.ts
|
|
1934
|
+
var MAX_CAPTURE_LOG_RECORDS = 5e3;
|
|
1935
|
+
var CAPTURE_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
1936
|
+
function captureLogPath(projectRoot) {
|
|
1937
|
+
return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
|
|
1938
|
+
}
|
|
1939
|
+
function appendCaptureRecord(projectRoot, record, env = process.env) {
|
|
1940
|
+
if (!isTelemetryEnabled(env)) return;
|
|
1941
|
+
appendJsonl(captureLogPath(projectRoot), record, {
|
|
1942
|
+
maxRecords: MAX_CAPTURE_LOG_RECORDS,
|
|
1943
|
+
trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
|
|
1947
|
+
if (!isTelemetryEnabled(env)) return;
|
|
1948
|
+
const session = sessionId(env);
|
|
1949
|
+
appendCaptureRecord(
|
|
1950
|
+
projectRoot,
|
|
1951
|
+
{
|
|
1952
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1953
|
+
isNewLesson: result?.isNewLesson ?? false,
|
|
1954
|
+
isNewTopic: result?.isNewTopic ?? false,
|
|
1955
|
+
newTriggerCount: result?.newTriggerIds.length ?? 0,
|
|
1956
|
+
triggerKinds,
|
|
1957
|
+
blocked: result === null,
|
|
1958
|
+
warningCodes: result?.warnings.map((w) => w.code) ?? [],
|
|
1959
|
+
...session !== void 0 ? { session } : {},
|
|
1960
|
+
...result !== null ? { lessonId: result.id } : {}
|
|
1961
|
+
},
|
|
1962
|
+
env
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
function normalizeRecallFile(file, projectRoot) {
|
|
1966
|
+
const forward = file.replaceAll("\\", "/");
|
|
1967
|
+
const direct = relativize(projectRoot, forward);
|
|
1968
|
+
if (!direct.startsWith("../")) return direct;
|
|
1969
|
+
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
1970
|
+
return viaReal.startsWith("../") ? direct : viaReal;
|
|
1971
|
+
}
|
|
1972
|
+
function relativize(root, forward) {
|
|
1973
|
+
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
1974
|
+
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
1975
|
+
}
|
|
1976
|
+
function safeRealpath(path) {
|
|
1977
|
+
try {
|
|
1978
|
+
return realpathSync(path);
|
|
1979
|
+
} catch {
|
|
1980
|
+
const parent = dirname(path);
|
|
1981
|
+
if (parent === path) return path;
|
|
1982
|
+
return resolve(safeRealpath(parent), basename(path));
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
1986
|
+
var MAX_FILES = 2e5;
|
|
1987
|
+
function listProjectFiles(projectRoot) {
|
|
1988
|
+
const out = /* @__PURE__ */ new Set();
|
|
1989
|
+
try {
|
|
1990
|
+
const stack = [projectRoot];
|
|
1991
|
+
while (stack.length > 0) {
|
|
1992
|
+
const dir = stack.pop();
|
|
1993
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1994
|
+
if (entry.isDirectory()) {
|
|
1995
|
+
if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
1996
|
+
} else if (entry.isFile()) {
|
|
1997
|
+
out.add(toRelPath(projectRoot, join(dir, entry.name)));
|
|
1998
|
+
if (out.size > MAX_FILES) return out;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
return out;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/lessons/keyword-match.ts
|
|
2009
|
+
function splitTokens(text) {
|
|
2010
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
2011
|
+
}
|
|
2012
|
+
function deriveHaystackTokens(query) {
|
|
2013
|
+
const parts = [];
|
|
2014
|
+
if (query.file !== void 0) parts.push(query.file);
|
|
2015
|
+
if (query.command !== void 0) parts.push(query.command);
|
|
2016
|
+
return parts.length === 0 ? [] : splitTokens(parts.join(" "));
|
|
2017
|
+
}
|
|
2018
|
+
function containsRun(needle, hay) {
|
|
2019
|
+
if (needle.length === 0) return false;
|
|
2020
|
+
for (let i = 0; i + needle.length <= hay.length; i += 1) {
|
|
2021
|
+
let hit = true;
|
|
2022
|
+
for (let j = 0; j < needle.length; j += 1) {
|
|
2023
|
+
if (hay[i + j] !== needle[j]) {
|
|
2024
|
+
hit = false;
|
|
2025
|
+
break;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
if (hit) return true;
|
|
2029
|
+
}
|
|
2030
|
+
return false;
|
|
2031
|
+
}
|
|
2032
|
+
function keywordMatches(pattern, query) {
|
|
2033
|
+
if (query.keyword !== void 0 && query.keyword.toLowerCase().includes(pattern.toLowerCase())) {
|
|
2034
|
+
return true;
|
|
2035
|
+
}
|
|
2036
|
+
return containsRun(tokenize(pattern), deriveHaystackTokens(query));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/lessons/query.ts
|
|
2040
|
+
var COMMAND_MATCH_BUDGET = 5e6;
|
|
2041
|
+
function queryLessons(graph, query) {
|
|
2042
|
+
if (query.file === void 0 && query.command === void 0 && query.keyword === void 0) {
|
|
2043
|
+
return [];
|
|
2044
|
+
}
|
|
2045
|
+
const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
|
|
2046
|
+
const matched = [];
|
|
2047
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2048
|
+
if (lesson.status !== "active") continue;
|
|
2049
|
+
if (lesson.triggers.some((t) => matchedTriggerIds.has(t))) {
|
|
2050
|
+
matched.push({ id, lesson });
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
matched.sort((a, b) => a.id < b.id ? -1 : 1);
|
|
2054
|
+
return matched;
|
|
2055
|
+
}
|
|
2056
|
+
function collectMatchedTriggersByKind(graph, query) {
|
|
2057
|
+
const byKind = {
|
|
2058
|
+
file_glob: /* @__PURE__ */ new Set(),
|
|
2059
|
+
command_pattern: /* @__PURE__ */ new Set(),
|
|
2060
|
+
keyword: /* @__PURE__ */ new Set()
|
|
2061
|
+
};
|
|
2062
|
+
const budget = { remaining: COMMAND_MATCH_BUDGET };
|
|
2063
|
+
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
2064
|
+
if (triggerMatches(trigger, query, budget)) byKind[trigger.kind].add(id);
|
|
2065
|
+
}
|
|
2066
|
+
return byKind;
|
|
2067
|
+
}
|
|
2068
|
+
function collectMatchedTriggerIds(graph, query) {
|
|
2069
|
+
const { file_glob, command_pattern, keyword } = collectMatchedTriggersByKind(graph, query);
|
|
2070
|
+
return /* @__PURE__ */ new Set([...file_glob, ...command_pattern, ...keyword]);
|
|
2071
|
+
}
|
|
2072
|
+
function triggerMatches(trigger, query, budget) {
|
|
2073
|
+
switch (trigger.kind) {
|
|
2074
|
+
case "file_glob":
|
|
2075
|
+
if (query.file === void 0) return false;
|
|
2076
|
+
return picomatch(trigger.pattern, { dot: true })(query.file);
|
|
2077
|
+
case "command_pattern": {
|
|
2078
|
+
if (query.command === void 0) return false;
|
|
2079
|
+
const matcher = getCommandMatcher(trigger.pattern);
|
|
2080
|
+
return matcher !== null && matcher.test(query.command, budget);
|
|
2081
|
+
}
|
|
2082
|
+
case "keyword":
|
|
2083
|
+
return keywordMatches(trigger.pattern, query);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/lessons/ranking.ts
|
|
2088
|
+
var DEFAULT_RECALL_LIMIT = 10;
|
|
2089
|
+
var DEFAULT_RECALL_MAX_TOKENS = 400;
|
|
2090
|
+
var RRF_K = 60;
|
|
2091
|
+
var SPECIFICITY_WEIGHT = 3;
|
|
2092
|
+
var TOPIC_COHERENCE_WEIGHT = 2;
|
|
2093
|
+
var BM25_WEIGHT = 1;
|
|
2094
|
+
function rankMap(items) {
|
|
2095
|
+
const sorted = [...items].sort(
|
|
2096
|
+
(a, b) => b.value !== a.value ? b.value - a.value : a.id < b.id ? -1 : 1
|
|
2097
|
+
);
|
|
2098
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
2099
|
+
let prevValue = null;
|
|
2100
|
+
let prevRank = 0;
|
|
2101
|
+
sorted.forEach((item, i) => {
|
|
2102
|
+
const rank = prevValue !== null && item.value === prevValue ? prevRank : i + 1;
|
|
2103
|
+
ranks.set(item.id, rank);
|
|
2104
|
+
prevValue = item.value;
|
|
2105
|
+
prevRank = rank;
|
|
2106
|
+
});
|
|
2107
|
+
return ranks;
|
|
2108
|
+
}
|
|
2109
|
+
function estTokens(rule) {
|
|
2110
|
+
return Math.ceil(rule.length / 4);
|
|
2111
|
+
}
|
|
2112
|
+
function rankLessons(graph, query, matches, options = {}) {
|
|
2113
|
+
if (matches.length === 0) return [];
|
|
2114
|
+
const terms = queryTerms(query);
|
|
2115
|
+
const corpus = buildCorpus(graph);
|
|
2116
|
+
const fanout = buildFanout(graph);
|
|
2117
|
+
const coherence = buildTopicCoherence(matches);
|
|
2118
|
+
const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
|
|
2119
|
+
const scored = matches.map(({ id, lesson }) => {
|
|
2120
|
+
const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
|
|
2121
|
+
let specificity = 0;
|
|
2122
|
+
for (const t of hitTriggers) specificity = Math.max(specificity, 1 / fanout.get(t));
|
|
2123
|
+
return {
|
|
2124
|
+
id,
|
|
2125
|
+
lesson,
|
|
2126
|
+
bm25: bm25(terms, lesson.rule, corpus),
|
|
2127
|
+
specificity,
|
|
2128
|
+
// `id` is a matched lesson, and buildTopicCoherence keys every matched id.
|
|
2129
|
+
topicCoherence: coherence.get(id),
|
|
2130
|
+
matchedTriggers: hitTriggers
|
|
2131
|
+
};
|
|
2132
|
+
});
|
|
2133
|
+
const bm25Ranks = rankMap(scored.map((s) => ({ id: s.id, value: s.bm25 })));
|
|
2134
|
+
const specRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.specificity })));
|
|
2135
|
+
const topicRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.topicCoherence })));
|
|
2136
|
+
const ranked = scored.map((s) => ({
|
|
2137
|
+
id: s.id,
|
|
2138
|
+
lesson: s.lesson,
|
|
2139
|
+
score: SPECIFICITY_WEIGHT / (RRF_K + specRanks.get(s.id)) + TOPIC_COHERENCE_WEIGHT / (RRF_K + topicRanks.get(s.id)) + BM25_WEIGHT / (RRF_K + bm25Ranks.get(s.id)),
|
|
2140
|
+
reason: {
|
|
2141
|
+
matchedTriggers: s.matchedTriggers,
|
|
2142
|
+
bm25: s.bm25,
|
|
2143
|
+
specificity: s.specificity,
|
|
2144
|
+
topicCoherence: s.topicCoherence
|
|
2145
|
+
}
|
|
2146
|
+
})).sort((a, b) => {
|
|
2147
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
2148
|
+
const ca = a.lesson.createdAt;
|
|
2149
|
+
const cb = b.lesson.createdAt;
|
|
2150
|
+
if (ca !== cb) return ca < cb ? 1 : -1;
|
|
2151
|
+
return a.id < b.id ? -1 : 1;
|
|
2152
|
+
});
|
|
2153
|
+
return applyCaps(ranked, options);
|
|
2154
|
+
}
|
|
2155
|
+
function applyCaps(ranked, options) {
|
|
2156
|
+
let out = ranked;
|
|
2157
|
+
if (options.limit !== void 0 && options.limit >= 0) out = out.slice(0, options.limit);
|
|
2158
|
+
if (options.maxTokens !== void 0 && out.length > 0) {
|
|
2159
|
+
const budgeted = [out[0]];
|
|
2160
|
+
let used = estTokens(out[0].lesson.rule);
|
|
2161
|
+
for (const row of out.slice(1)) {
|
|
2162
|
+
const cost = estTokens(row.lesson.rule);
|
|
2163
|
+
if (used + cost > options.maxTokens) break;
|
|
2164
|
+
used += cost;
|
|
2165
|
+
budgeted.push(row);
|
|
2166
|
+
}
|
|
2167
|
+
out = budgeted;
|
|
2168
|
+
}
|
|
2169
|
+
return out;
|
|
2170
|
+
}
|
|
2171
|
+
function defaultLessonsConfig() {
|
|
2172
|
+
return {
|
|
2173
|
+
recallLimit: DEFAULT_RECALL_LIMIT,
|
|
2174
|
+
recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
|
|
2175
|
+
autoPrune: false
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
function positiveInt(value) {
|
|
2179
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
2180
|
+
}
|
|
2181
|
+
function loadRecallConfig(projectRoot) {
|
|
2182
|
+
const fallback = {
|
|
2183
|
+
limit: DEFAULT_RECALL_LIMIT,
|
|
2184
|
+
maxTokens: DEFAULT_RECALL_MAX_TOKENS
|
|
2185
|
+
};
|
|
2186
|
+
const path = lessonsPaths(projectRoot).config;
|
|
2187
|
+
if (!existsSync(path)) return fallback;
|
|
2188
|
+
try {
|
|
2189
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2190
|
+
if (typeof parsed !== "object" || parsed === null) return fallback;
|
|
2191
|
+
const cfg = parsed;
|
|
2192
|
+
return {
|
|
2193
|
+
limit: positiveInt(cfg.recallLimit) ?? fallback.limit,
|
|
2194
|
+
maxTokens: positiveInt(cfg.recallMaxTokens) ?? fallback.maxTokens
|
|
2195
|
+
};
|
|
2196
|
+
} catch {
|
|
2197
|
+
return fallback;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
var SEEN_DIR = "agentsmesh-lessons-seen";
|
|
2201
|
+
function openSessionDedup(options = {}) {
|
|
2202
|
+
if (options.disabled === true) return null;
|
|
2203
|
+
const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
|
|
2204
|
+
if (id === void 0) return null;
|
|
2205
|
+
const path = seenPath(id);
|
|
2206
|
+
return { sessionId: id, seen: loadSeen(path), path };
|
|
2207
|
+
}
|
|
2208
|
+
function seenPath(id) {
|
|
2209
|
+
const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 200);
|
|
2210
|
+
return join(tmpdir(), SEEN_DIR, `${safe}.json`);
|
|
2211
|
+
}
|
|
2212
|
+
function loadSeen(path) {
|
|
2213
|
+
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
2214
|
+
try {
|
|
2215
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2216
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
2217
|
+
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
2218
|
+
} catch {
|
|
2219
|
+
return /* @__PURE__ */ new Set();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function filterUnseen(dedup, matches) {
|
|
2223
|
+
return matches.filter((m) => !dedup.seen.has(m.id));
|
|
2224
|
+
}
|
|
2225
|
+
function commitSeen(dedup, returnedIds) {
|
|
2226
|
+
if (returnedIds.length === 0) return;
|
|
2227
|
+
const union3 = new Set(dedup.seen);
|
|
2228
|
+
for (const id of returnedIds) union3.add(id);
|
|
2229
|
+
if (union3.size === dedup.seen.size) return;
|
|
2230
|
+
try {
|
|
2231
|
+
mkdirSync(dirname(dedup.path), { recursive: true });
|
|
2232
|
+
const tmp = `${dedup.path}.${process.pid}.tmp`;
|
|
2233
|
+
writeFileSync(tmp, JSON.stringify([...union3]), "utf8");
|
|
2234
|
+
renameSync(tmp, dedup.path);
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// src/lessons/recall.ts
|
|
2240
|
+
async function recallLessons(projectRoot, query, options = {}) {
|
|
2241
|
+
try {
|
|
2242
|
+
await maybeAutoMigrateLessons(projectRoot);
|
|
2243
|
+
} catch {
|
|
2244
|
+
}
|
|
2245
|
+
const load = loadLessonsGraphResilient(projectRoot);
|
|
2246
|
+
if (load.status === "corrupt") {
|
|
2247
|
+
return { lessons: [], totalMatches: 0, suppressed: 0, corrupt: true };
|
|
2248
|
+
}
|
|
2249
|
+
if (load.status === "newer-version") {
|
|
2250
|
+
return { lessons: [], totalMatches: 0, suppressed: 0, newerVersion: load.version };
|
|
2251
|
+
}
|
|
2252
|
+
if (load.status === "absent") return { lessons: [], totalMatches: 0, suppressed: 0 };
|
|
2253
|
+
const graph = load.graph;
|
|
2254
|
+
const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
|
|
2255
|
+
const matches = queryLessons(graph, matchQuery);
|
|
2256
|
+
const dedup = openSessionDedup({ explicit: options.sessionId, disabled: options.noDedup });
|
|
2257
|
+
const forRank = dedup === null ? matches : filterUnseen(dedup, matches);
|
|
2258
|
+
const cfg = loadRecallConfig(projectRoot);
|
|
2259
|
+
const lessons = rankLessons(graph, matchQuery, forRank, {
|
|
2260
|
+
limit: options.limit ?? cfg.limit,
|
|
2261
|
+
maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens
|
|
2262
|
+
});
|
|
2263
|
+
if (dedup !== null) commitSeen(dedup, lessons.map((l) => l.id));
|
|
2264
|
+
recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, { bypassed: false });
|
|
2265
|
+
return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
|
|
2266
|
+
}
|
|
2267
|
+
function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
|
|
2268
|
+
if (!isTelemetryEnabled()) return;
|
|
2269
|
+
const byKind = collectMatchedTriggersByKind(graph, query);
|
|
2270
|
+
const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
|
|
2271
|
+
const session = sessionId();
|
|
2272
|
+
appendRecallRecord(projectRoot, {
|
|
2273
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2274
|
+
hasFile: query.file !== void 0,
|
|
2275
|
+
hasCommand: query.command !== void 0,
|
|
2276
|
+
hasKeyword: query.keyword !== void 0,
|
|
2277
|
+
totalMatches: matches.length,
|
|
2278
|
+
returnedCount: lessons.length,
|
|
2279
|
+
returnedTokens: lessons.reduce((sum, l) => sum + estTokens(l.lesson.rule), 0),
|
|
2280
|
+
truncated: matches.length > lessons.length,
|
|
2281
|
+
matchedByKind: {
|
|
2282
|
+
file: countVia(byKind.file_glob),
|
|
2283
|
+
command: countVia(byKind.command_pattern),
|
|
2284
|
+
keyword: countVia(byKind.keyword)
|
|
2285
|
+
},
|
|
2286
|
+
lessonIds: lessons.map((l) => l.id),
|
|
2287
|
+
bypassed: options.bypassed === true,
|
|
2288
|
+
...session !== void 0 ? { session } : {}
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
async function captureLesson(projectRoot, input, options = {}) {
|
|
2292
|
+
await maybeAutoMigrateLessons(projectRoot);
|
|
2293
|
+
const triggerKinds = {
|
|
2294
|
+
file: input.triggers.files?.length ?? 0,
|
|
2295
|
+
command: input.triggers.commands?.length ?? 0,
|
|
2296
|
+
keyword: input.triggers.keywords?.length ?? 0
|
|
2297
|
+
};
|
|
2298
|
+
const knownPaths = options.knownPaths ?? listProjectFiles(projectRoot) ?? void 0;
|
|
2299
|
+
try {
|
|
2300
|
+
const result = await addLesson(projectRoot, input, { ...options, knownPaths });
|
|
2301
|
+
recordCapture(projectRoot, triggerKinds, result);
|
|
2302
|
+
const autoPruned = await maybeAutoPrune(projectRoot, knownPaths);
|
|
2303
|
+
return autoPruned === null ? result : { ...result, autoPruned };
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
recordCapture(projectRoot, triggerKinds, null);
|
|
2306
|
+
throw err;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// src/lessons/merge.ts
|
|
2311
|
+
async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
|
|
2312
|
+
return mutateLessonsGraph(projectRoot, (graph) => mergeInto(graph, loserId, keeperId), {
|
|
2313
|
+
retries: options.retries
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
function mergeInto(graph, loserId, keeperId) {
|
|
2317
|
+
if (loserId === keeperId) {
|
|
2318
|
+
throw new Error(`mergeLessons: cannot merge lesson "${loserId}" into itself.`);
|
|
2319
|
+
}
|
|
2320
|
+
const loser = graph.lessons[loserId];
|
|
2321
|
+
if (loser === void 0) throw new Error(`mergeLessons: unknown lesson "${loserId}".`);
|
|
2322
|
+
const keeper = graph.lessons[keeperId];
|
|
2323
|
+
if (keeper === void 0) throw new Error(`mergeLessons: unknown lesson "${keeperId}".`);
|
|
2324
|
+
if (keeper.status !== "active") {
|
|
2325
|
+
throw new Error(`mergeLessons: keeper "${keeperId}" is not active (status: ${keeper.status}).`);
|
|
2326
|
+
}
|
|
2327
|
+
if (loser.status !== "active") {
|
|
2328
|
+
throw new Error(
|
|
2329
|
+
`mergeLessons: loser "${loserId}" is already ${loser.status}; nothing to merge.`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
graph.lessons[keeperId] = {
|
|
2333
|
+
...keeper,
|
|
2334
|
+
triggers: union2(keeper.triggers, loser.triggers),
|
|
2335
|
+
topics: union2(keeper.topics, loser.topics),
|
|
2336
|
+
evidence: union2(keeper.evidence, loser.evidence)
|
|
2337
|
+
};
|
|
2338
|
+
graph.lessons[loserId] = { ...loser, status: "superseded", supersededBy: keeperId };
|
|
2339
|
+
return { loserId, keeperId };
|
|
2340
|
+
}
|
|
2341
|
+
function union2(base, extra) {
|
|
2342
|
+
const out = [...base];
|
|
2343
|
+
for (const item of extra) {
|
|
2344
|
+
if (!out.includes(item)) out.push(item);
|
|
2345
|
+
}
|
|
2346
|
+
return out;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// src/lessons/strip-markers.ts
|
|
2350
|
+
var LINE_REFS = String.raw`L\d+(?:\s*,\s*L\d+)*`;
|
|
2351
|
+
var LINE_REF_PATTERNS = [
|
|
2352
|
+
new RegExp(String.raw`\s*\bSee\s+${LINE_REFS}\.?`, "g"),
|
|
2353
|
+
// " See L128." / " See L140, L149"
|
|
2354
|
+
new RegExp(String.raw`\s*\((?:${LINE_REFS})\)\.?`, "g"),
|
|
2355
|
+
// " (L174)" / " (L92, L163)"
|
|
2356
|
+
new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]\.?`, "g")
|
|
2357
|
+
// " [L161, L208]"
|
|
2358
|
+
];
|
|
2359
|
+
var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
|
|
2360
|
+
function stripLegacyMarkers(rule) {
|
|
2361
|
+
let out = rule;
|
|
2362
|
+
for (const pattern of LINE_REF_PATTERNS) out = out.replace(pattern, "");
|
|
2363
|
+
out = out.replace(ALSO_RELEVANT_PATTERN, " ");
|
|
2364
|
+
return out.trim();
|
|
2365
|
+
}
|
|
2366
|
+
function applyStrip(graph) {
|
|
2367
|
+
const changedIds = [];
|
|
2368
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2369
|
+
const stripped = stripLegacyMarkers(lesson.rule);
|
|
2370
|
+
if (stripped === lesson.rule || stripped.length === 0) continue;
|
|
2371
|
+
changedIds.push(id);
|
|
2372
|
+
graph.lessons[id] = { ...lesson, rule: stripped };
|
|
2373
|
+
}
|
|
2374
|
+
return changedIds.sort();
|
|
2375
|
+
}
|
|
2376
|
+
async function stripMarkersInGraph(projectRoot, options = {}) {
|
|
2377
|
+
await maybeAutoMigrateLessons(projectRoot);
|
|
2378
|
+
const existing = tryLoadLessonsGraph(projectRoot);
|
|
2379
|
+
if (existing === null) return { changedIds: [], changedCount: 0 };
|
|
2380
|
+
if (options.dryRun === true) {
|
|
2381
|
+
const changedIds2 = applyStrip(existing);
|
|
2382
|
+
return { changedIds: changedIds2, changedCount: changedIds2.length };
|
|
2383
|
+
}
|
|
2384
|
+
let changedIds = [];
|
|
2385
|
+
await mutateLessonsGraph(projectRoot, (graph) => {
|
|
2386
|
+
changedIds = applyStrip(graph);
|
|
2387
|
+
});
|
|
2388
|
+
return { changedIds, changedCount: changedIds.length };
|
|
2389
|
+
}
|
|
2390
|
+
var RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
|
|
2391
|
+
var RECALL_HOOK_MATCHER = "Edit|Write|Bash";
|
|
2392
|
+
function injectRecallHook(projectRoot) {
|
|
2393
|
+
const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
|
|
2394
|
+
if (!existsSync(path)) return false;
|
|
2395
|
+
const doc = parseDocument(readFileSync(path, "utf8"));
|
|
2396
|
+
const existing = doc.get("PostToolUse");
|
|
2397
|
+
const post = existing instanceof YAMLSeq ? existing : new YAMLSeq();
|
|
2398
|
+
const present = post.items.some(
|
|
2399
|
+
(item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
|
|
2400
|
+
);
|
|
2401
|
+
if (present) return false;
|
|
2402
|
+
post.add(
|
|
2403
|
+
doc.createNode({ matcher: RECALL_HOOK_MATCHER, type: "command", command: RECALL_HOOK_COMMAND })
|
|
2404
|
+
);
|
|
2405
|
+
doc.set("PostToolUse", post);
|
|
2406
|
+
writeFileSync(path, String(doc), "utf8");
|
|
2407
|
+
return true;
|
|
2408
|
+
}
|
|
2409
|
+
var UTF8_BOM = "\uFEFF";
|
|
2410
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2411
|
+
".md",
|
|
2412
|
+
".mdc",
|
|
2413
|
+
".mdx",
|
|
2414
|
+
".markdown",
|
|
2415
|
+
".txt",
|
|
2416
|
+
".json",
|
|
2417
|
+
".jsonc",
|
|
2418
|
+
".yaml",
|
|
2419
|
+
".yml",
|
|
2420
|
+
".toml",
|
|
2421
|
+
".ini",
|
|
2422
|
+
".sh",
|
|
2423
|
+
".bash",
|
|
2424
|
+
".zsh",
|
|
2425
|
+
".ps1",
|
|
2426
|
+
".js",
|
|
2427
|
+
".mjs",
|
|
2428
|
+
".cjs",
|
|
2429
|
+
".ts",
|
|
2430
|
+
".tsx",
|
|
2431
|
+
".html",
|
|
2432
|
+
".css"
|
|
2433
|
+
]);
|
|
2434
|
+
var TEXT_DOTFILES = /* @__PURE__ */ new Set([
|
|
2435
|
+
".gitignore",
|
|
2436
|
+
".cursorignore",
|
|
2437
|
+
".cursorindexingignore",
|
|
2438
|
+
".aiignore",
|
|
2439
|
+
".agentignore",
|
|
2440
|
+
".clineignore",
|
|
2441
|
+
".geminiignore",
|
|
2442
|
+
".codeiumignore",
|
|
2443
|
+
".continueignore",
|
|
2444
|
+
".copilotignore",
|
|
2445
|
+
".windsurfignore",
|
|
2446
|
+
".junieignore",
|
|
2447
|
+
".kiroignore",
|
|
2448
|
+
".rooignore",
|
|
2449
|
+
".antigravityignore"
|
|
2450
|
+
]);
|
|
2451
|
+
function shouldNormalizeLineEndings(path) {
|
|
2452
|
+
const ext = extname(path).toLowerCase();
|
|
2453
|
+
if (ext.length > 0) return TEXT_EXTENSIONS.has(ext);
|
|
2454
|
+
const base = basename(path).toLowerCase();
|
|
2455
|
+
return TEXT_DOTFILES.has(base);
|
|
2456
|
+
}
|
|
2457
|
+
function normalizeLineEndings(content) {
|
|
2458
|
+
return content.replace(/\r\n?/g, "\n");
|
|
2459
|
+
}
|
|
2460
|
+
var EXECUTABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh"]);
|
|
2461
|
+
function executableModeFor(path) {
|
|
2462
|
+
return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/utils/filesystem/fs.ts
|
|
2466
|
+
async function readFileSafe(path) {
|
|
2467
|
+
try {
|
|
2468
|
+
const data = await readFile(path, "utf-8");
|
|
2469
|
+
return data.startsWith(UTF8_BOM) ? data.slice(UTF8_BOM.length) : data;
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
const e = err;
|
|
2472
|
+
if (e.code === "ENOENT") return null;
|
|
2473
|
+
throw new FileSystemError(
|
|
2474
|
+
path,
|
|
2475
|
+
`Failed to read ${path}: ${e.message}. Ensure the file exists and is readable.`,
|
|
2476
|
+
{ cause: err, errnoCode: e.code }
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
async function writeFileAtomic(path, content, options) {
|
|
2481
|
+
const dir = dirname(path);
|
|
2482
|
+
await mkdir(dir, { recursive: true });
|
|
2483
|
+
try {
|
|
2484
|
+
const info = await lstat(path);
|
|
2485
|
+
if (info.isDirectory()) {
|
|
2486
|
+
throw new FileSystemError(
|
|
2487
|
+
path,
|
|
2488
|
+
`Failed to write ${path}: target exists and is a directory. Remove it or choose a different path.`,
|
|
2489
|
+
{ errnoCode: "EISDIR" }
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
if (info.isSymbolicLink()) {
|
|
2493
|
+
await unlink(path).catch((e) => {
|
|
2494
|
+
if (e.code !== "ENOENT") throw e;
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
if (err instanceof FileSystemError) throw err;
|
|
2499
|
+
const e = err;
|
|
2500
|
+
if (e.code !== "ENOENT") throw err;
|
|
2501
|
+
}
|
|
2502
|
+
const tmpPath = `${path}.tmp`;
|
|
2503
|
+
const payload = shouldNormalizeLineEndings(path) ? normalizeLineEndings(content) : content;
|
|
2504
|
+
const mode = executableModeFor(path);
|
|
2505
|
+
try {
|
|
2506
|
+
try {
|
|
2507
|
+
const tmpInfo = await lstat(tmpPath);
|
|
2508
|
+
if (tmpInfo.isSymbolicLink()) {
|
|
2509
|
+
await unlink(tmpPath);
|
|
2510
|
+
}
|
|
2511
|
+
} catch (tmpErr) {
|
|
2512
|
+
if (tmpErr.code !== "ENOENT") throw tmpErr;
|
|
2513
|
+
}
|
|
2514
|
+
const writeOpts = {
|
|
2515
|
+
encoding: "utf-8",
|
|
2516
|
+
flag: "w"
|
|
2517
|
+
};
|
|
2518
|
+
if (mode !== void 0) writeOpts.mode = mode;
|
|
2519
|
+
await writeFile(tmpPath, payload, writeOpts);
|
|
2520
|
+
await rename(tmpPath, path);
|
|
2521
|
+
if (mode !== void 0) {
|
|
2522
|
+
await chmod(path, mode);
|
|
2523
|
+
}
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
await rm(tmpPath, { force: true }).catch(() => {
|
|
2526
|
+
});
|
|
2527
|
+
const e = err;
|
|
2528
|
+
throw new FileSystemError(
|
|
2529
|
+
path,
|
|
2530
|
+
`Failed to write ${path}: ${e.message}. Check permissions and disk space.`,
|
|
2531
|
+
{ cause: err, errnoCode: e.code }
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// src/utils/filesystem/gitignore.ts
|
|
2537
|
+
async function ensureGitignoreEntries(projectRoot, entries) {
|
|
2538
|
+
const gitignorePath = join(projectRoot, ".gitignore");
|
|
2539
|
+
const current = await readFileSafe(gitignorePath) ?? "";
|
|
2540
|
+
const existing = new Set(
|
|
2541
|
+
current.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("#"))
|
|
2542
|
+
);
|
|
2543
|
+
const toAdd = entries.filter((e) => !isCoveredByExisting(e, existing));
|
|
2544
|
+
if (toAdd.length === 0) return false;
|
|
2545
|
+
const suffix = current.endsWith("\n") || current === "" ? "" : "\n";
|
|
2546
|
+
await writeFileAtomic(gitignorePath, current + suffix + toAdd.join("\n") + "\n");
|
|
2547
|
+
return true;
|
|
2548
|
+
}
|
|
2549
|
+
function isCoveredByExisting(candidate, existing) {
|
|
2550
|
+
if (existing.has(candidate)) return true;
|
|
2551
|
+
let parent = candidate.replace(/\/$/, "");
|
|
2552
|
+
while (parent.includes("/")) {
|
|
2553
|
+
parent = parent.slice(0, parent.lastIndexOf("/"));
|
|
2554
|
+
if (parent === "") break;
|
|
2555
|
+
if (existing.has(parent) || existing.has(`${parent}/`) || existing.has(`${parent}/**`)) {
|
|
2556
|
+
return true;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
var LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
|
|
2562
|
+
var LESSONS_CONTRACT_END = "<!-- agentsmesh:lessons-contract:end -->";
|
|
2563
|
+
function escapeRegExp(value) {
|
|
2564
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2565
|
+
}
|
|
2566
|
+
function managedBlockPattern(start, end) {
|
|
2567
|
+
return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, "g");
|
|
2568
|
+
}
|
|
2569
|
+
function stripManagedBlock(content, start, end) {
|
|
2570
|
+
return content.replace(managedBlockPattern(start, end), "").trim();
|
|
2571
|
+
}
|
|
2572
|
+
function splitFrontmatterPrefix(content) {
|
|
2573
|
+
if (content.indexOf("---") !== 0) return { prefix: "", body: content.trim() };
|
|
2574
|
+
const close = content.indexOf("---", 3);
|
|
2575
|
+
if (close === -1) return { prefix: "", body: content.trim() };
|
|
2576
|
+
return { prefix: content.slice(0, close + 3), body: content.slice(close + 3).trim() };
|
|
2577
|
+
}
|
|
2578
|
+
function insertAtBodyTop(content, block) {
|
|
2579
|
+
const { prefix, body } = splitFrontmatterPrefix(content);
|
|
2580
|
+
const placed = body ? `${block}
|
|
2581
|
+
|
|
2582
|
+
${body}` : block;
|
|
2583
|
+
return prefix ? `${prefix}
|
|
2584
|
+
|
|
2585
|
+
${placed}` : placed;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// src/targets/projection/lessons-paragraph.ts
|
|
2589
|
+
var LESSONS_PARAGRAPH_BLOCK = `${LESSONS_CONTRACT_START}
|
|
2590
|
+
${LESSONS_PROCEDURAL_RULE}
|
|
2591
|
+
${LESSONS_CONTRACT_END}`;
|
|
2592
|
+
function appendLessonsParagraph(content) {
|
|
2593
|
+
const withoutPrior = stripLessonsParagraph(content);
|
|
2594
|
+
return insertAtBodyTop(withoutPrior, LESSONS_PARAGRAPH_BLOCK);
|
|
2595
|
+
}
|
|
2596
|
+
function stripLessonsParagraph(content) {
|
|
2597
|
+
const withoutBlock = stripManagedBlock(content, LESSONS_CONTRACT_START, LESSONS_CONTRACT_END);
|
|
2598
|
+
return stripRawProceduralRule(withoutBlock).trim();
|
|
2599
|
+
}
|
|
2600
|
+
function stripRawProceduralRule(content) {
|
|
2601
|
+
return content.replace(`
|
|
2602
|
+
|
|
2603
|
+
${LESSONS_PROCEDURAL_RULE}`, "").replace(LESSONS_PROCEDURAL_RULE, "");
|
|
2604
|
+
}
|
|
2605
|
+
function serializeFrontmatter(frontmatter, body) {
|
|
2606
|
+
const keys = Object.keys(frontmatter);
|
|
2607
|
+
if (keys.length === 0) return body;
|
|
2608
|
+
const yamlStr = stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
2609
|
+
return `---
|
|
2610
|
+
${yamlStr}
|
|
2611
|
+
---
|
|
2612
|
+
|
|
2613
|
+
${body}`;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// src/lessons/skill.ts
|
|
2617
|
+
var LESSONS_SKILL_NAME = "lessons";
|
|
2618
|
+
var LESSONS_SKILL_DESCRIPTION = "Full operating manual for the agentsmesh lessons system (recall + capture). Consult when running any `agentsmesh lessons` subcommand (query, add, topics, show, deprecate, merge, untrigger, strip-markers, journal, validate, stats, prune, import-md), choosing a topic or trigger flags, using the lessons MCP tools, or when unsure how to phrase or capture a lesson.";
|
|
2619
|
+
var LESSONS_SKILL_BODY = `# Lessons \u2014 operating manual
|
|
2620
|
+
|
|
2621
|
+
Two commands: **Recall** before you act, **Capture** after any failure. The graph
|
|
2622
|
+
\`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
|
|
2623
|
+
|
|
2624
|
+
## Recall \u2014 before each file edit and each state-changing command
|
|
2625
|
+
|
|
2626
|
+
\`agentsmesh lessons query --file <path> --cmd <command>\` (add \`--keyword <text>\` to
|
|
2627
|
+
match by task), then apply every rule returned. Scope is MUTATING actions: file edits
|
|
2628
|
+
and state-changing commands (build/test/install/migrate/git-write). Pure-read commands
|
|
2629
|
+
(cat/ls/grep/git-log; read-only) and the recall query itself are **exempt** \u2014 no
|
|
2630
|
+
infinite regress. A predicate-less query is rejected; **keyword-only recall is the
|
|
2631
|
+
anti-pattern** \u2014 most lessons are keyed to a \`file_glob\`/\`command_pattern\` and won't
|
|
2632
|
+
surface (the CLI warns). Excuses ("small edit", "I already know this", "later") all
|
|
2633
|
+
mean: query first \u2014 skipping recall on a mutating action is a process violation, and
|
|
2634
|
+
the user will check.
|
|
2635
|
+
|
|
2636
|
+
## Capture \u2014 immediately after any failure
|
|
2637
|
+
|
|
2638
|
+
Any failure counts, not just red tests: a failing test/CI/lint/typecheck, a code
|
|
2639
|
+
review, a user correction, a regression, or a wrong assumption \u2014 yours or anyone's.
|
|
2640
|
+
|
|
2641
|
+
\`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`
|
|
2642
|
+
|
|
2643
|
+
- **At least one _effective_ trigger is required.** A capture is rejected
|
|
2644
|
+
(\`UNRECALLABLE_LESSON\`) when EVERY trigger is dead on the mandatory \`--file\`/\`--cmd\`
|
|
2645
|
+
recall path \u2014 a stopword-only keyword ("state of the art"), or an invalid/ReDoS
|
|
2646
|
+
command regex \u2014 because the lesson could never be recalled there. Prefer
|
|
2647
|
+
\`--trigger-file\`: the most reliable trigger, it fires on \`--file\` recall. A keyword
|
|
2648
|
+
alone is discouraged (\`KEYWORD_ONLY_LESSON\`); paraphrasing an existing rule warns
|
|
2649
|
+
(\`NEAR_DUPLICATE_LESSON\` \u2014 update that lesson instead).
|
|
2650
|
+
- **One imperative sentence.** A rule over 2000 chars is rejected (\`OVERSIZED_RULE\`) \u2014
|
|
2651
|
+
trim it or split into separate lessons; don't paste a log/diff.
|
|
2652
|
+
- Widen with \`--trigger-cmd <regex>\` / \`--trigger-kw <text>\`. New area:
|
|
2653
|
+
\`--new-topic --topic-summary "<line>"\` (list ids with \`agentsmesh lessons topics\`).
|
|
2654
|
+
|
|
2655
|
+
## No shell? \u2014 MCP tools
|
|
2656
|
+
|
|
2657
|
+
\`lessons_query\`, \`lessons_add\`, \`lessons_topics\`, \`lessons_show\` (inspect a topic),
|
|
2658
|
+
\`lessons_deprecate\` (retire). validate / prune / merge / import-md are CLI-only.
|
|
2659
|
+
|
|
2660
|
+
## Other subcommands
|
|
2661
|
+
|
|
2662
|
+
\`agentsmesh lessons <cmd>\`: \`show\` \xB7 \`deprecate\` (\`--superseded-by\`) \xB7 \`merge\` \xB7
|
|
2663
|
+
\`untrigger\` \xB7 \`strip-markers\` \xB7 \`prune\` (\`--apply\`; trims over-cap triggers, GCs
|
|
2664
|
+
orphan triggers/topics) \xB7 \`journal\` \xB7 \`validate\` \xB7 \`stats\` \xB7 \`import-md\`. Full
|
|
2665
|
+
help: \`agentsmesh lessons --help\`.
|
|
2666
|
+
|
|
2667
|
+
## Config (\`.agentsmesh/lessons/config.json\`)
|
|
2668
|
+
|
|
2669
|
+
\`recallLimit\` / \`recallMaxTokens\` (canonical recall caps; per-call overrides
|
|
2670
|
+
\`--top\` / \`--max-tokens\`). \`recallMaxTokens\` is approximate \u2014 \`rule.length / 4\`,
|
|
2671
|
+
not a real tokenizer. \`autoPrune: true\` (default off) auto-GCs structural cruft
|
|
2672
|
+
after each capture \u2014 orphan triggers/topics + non-stranding dead globs, the safe
|
|
2673
|
+
half of \`prune\`; never trims/strands an active lesson, git-reversible.
|
|
2674
|
+
|
|
2675
|
+
## Dedup (opt-in)
|
|
2676
|
+
|
|
2677
|
+
Set \`--session <id>\` (or \`AGENTSMESH_SESSION_ID\`) and lessons already delivered this
|
|
2678
|
+
session are suppressed, so each recall carries only what is new (\`--no-dedup\` opts
|
|
2679
|
+
out). With no session id, recall is fully stateless \u2014 unchanged.`;
|
|
2680
|
+
var LESSONS_SKILL_FILE = serializeFrontmatter(
|
|
2681
|
+
{ name: LESSONS_SKILL_NAME, description: LESSONS_SKILL_DESCRIPTION },
|
|
2682
|
+
LESSONS_SKILL_BODY
|
|
2683
|
+
);
|
|
2684
|
+
|
|
2685
|
+
// src/lessons/init.ts
|
|
2686
|
+
async function scaffoldLessons(projectRoot) {
|
|
2687
|
+
const paths = lessonsPaths(projectRoot);
|
|
2688
|
+
const created = [];
|
|
2689
|
+
const updated = [];
|
|
2690
|
+
const skipped = [];
|
|
2691
|
+
mkdirSync(paths.base, { recursive: true });
|
|
2692
|
+
await maybeAutoMigrateLessons(projectRoot);
|
|
2693
|
+
if (existsSync(paths.graph)) {
|
|
2694
|
+
skipped.push(paths.graph);
|
|
2695
|
+
} else {
|
|
2696
|
+
await mutateLessonsGraphLocked(projectRoot, () => {
|
|
2697
|
+
});
|
|
2698
|
+
created.push(paths.graph);
|
|
2699
|
+
}
|
|
2700
|
+
seedLessonsConfig(projectRoot, created, skipped);
|
|
2701
|
+
seedLessonsSkill(projectRoot, created, updated, skipped);
|
|
2702
|
+
const rootRuleUpdated = injectProceduralBlock(projectRoot);
|
|
2703
|
+
const recallHookInjected = injectRecallHook(projectRoot);
|
|
2704
|
+
const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
|
|
2705
|
+
toRelPath(projectRoot, recallLogPath(projectRoot)),
|
|
2706
|
+
toRelPath(projectRoot, captureLogPath(projectRoot))
|
|
2707
|
+
]);
|
|
2708
|
+
return { created, updated, skipped, rootRuleUpdated, gitignoreUpdated, recallHookInjected };
|
|
2709
|
+
}
|
|
2710
|
+
function seedLessonsSkill(projectRoot, created, updated, skipped) {
|
|
2711
|
+
const skillPath = join(projectRoot, ".agentsmesh/skills", LESSONS_SKILL_NAME, "SKILL.md");
|
|
2712
|
+
const desired = `${LESSONS_SKILL_FILE}
|
|
2713
|
+
`;
|
|
2714
|
+
if (!existsSync(skillPath)) {
|
|
2715
|
+
mkdirSync(dirname(skillPath), { recursive: true });
|
|
2716
|
+
writeFileSync(skillPath, desired, "utf8");
|
|
2717
|
+
created.push(skillPath);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (readFileSync(skillPath, "utf8") === desired) {
|
|
2721
|
+
skipped.push(skillPath);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
writeFileSync(skillPath, desired, "utf8");
|
|
2725
|
+
updated.push(skillPath);
|
|
2726
|
+
}
|
|
2727
|
+
function seedLessonsConfig(projectRoot, created, skipped) {
|
|
2728
|
+
const configPath = lessonsPaths(projectRoot).config;
|
|
2729
|
+
if (existsSync(configPath)) {
|
|
2730
|
+
skipped.push(configPath);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
2734
|
+
writeFileSync(configPath, `${JSON.stringify(defaultLessonsConfig(), null, 2)}
|
|
2735
|
+
`, "utf8");
|
|
2736
|
+
created.push(configPath);
|
|
2737
|
+
}
|
|
2738
|
+
function injectProceduralBlock(projectRoot) {
|
|
2739
|
+
const rootRule = join(projectRoot, ".agentsmesh/rules/_root.md");
|
|
2740
|
+
if (!existsSync(rootRule)) {
|
|
2741
|
+
mkdirSync(dirname(rootRule), { recursive: true });
|
|
2742
|
+
const seeded = `---
|
|
2743
|
+
root: true
|
|
2744
|
+
description: ""
|
|
2745
|
+
---
|
|
2746
|
+
|
|
2747
|
+
${LESSONS_PARAGRAPH_BLOCK}
|
|
2748
|
+
|
|
2749
|
+
# Operational Guidelines
|
|
2750
|
+
`;
|
|
2751
|
+
writeFileSync(rootRule, seeded, "utf8");
|
|
2752
|
+
return true;
|
|
2753
|
+
}
|
|
2754
|
+
const current = readFileSync(rootRule, "utf8");
|
|
2755
|
+
const desired = `${appendLessonsParagraph(current)}
|
|
2756
|
+
`;
|
|
2757
|
+
if (desired === current) return false;
|
|
2758
|
+
writeFileSync(rootRule, desired, "utf8");
|
|
2759
|
+
return true;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
export { DEFAULT_RECALL_LIMIT, DEFAULT_RECALL_MAX_TOKENS, LESSONS_LOCK_FILENAME, LESSONS_PROCEDURAL_RULE, LessonsGraphExistsError, LessonsGraphSchema, UnknownTopicError, acquireLessonsLock, addLesson, captureLesson, graphFilePath, importLegacyLessons, isSafeRegexPattern, lessonsLockPath, lessonsPaths, loadLessonsGraph, maybeAutoMigrateLessons, mergeLessons, mutateLessonsGraph, parseGraph, queryLessons, rankLessons, recallLessons, scaffoldLessons, serializeGraph, stripLegacyMarkers, stripMarkersInGraph, toRelPath, tryLoadLessonsGraph, validateLessonsGraph };
|
|
2763
|
+
//# sourceMappingURL=lessons.js.map
|
|
2764
|
+
//# sourceMappingURL=lessons.js.map
|