agentsmesh 0.32.0 → 0.34.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 +335 -0
- package/README.md +96 -144
- package/dist/canonical.d.ts +2 -2
- package/dist/canonical.js +16921 -9982
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +285 -230
- package/dist/engine.d.ts +8 -2
- package/dist/engine.js +17605 -10483
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +17861 -10679
- package/dist/index.js.map +1 -1
- package/dist/{init-CrZhoNTj.d.ts → init-ChDbKSJ0.d.ts} +9 -9
- package/dist/lessons.d.ts +10 -2
- package/dist/lessons.js +1386 -1079
- package/dist/lessons.js.map +1 -1
- package/dist/{schema-CH_JJep8.d.ts → schema-BbywZEB9.d.ts} +6 -0
- package/dist/{target-descriptor-CaLUz7SR.d.ts → target-descriptor-DtgeHKPG.d.ts} +22 -2
- package/dist/targets.d.ts +3 -3
- package/dist/targets.js +16798 -9754
- package/dist/targets.js.map +1 -1
- package/package.json +28 -23
- package/schemas/agentsmesh.json +9 -0
- package/schemas/installs.json +3 -0
- package/schemas/pack.json +3 -0
package/dist/lessons.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, realpathSync, appendFileSync } from 'fs';
|
|
3
3
|
import { resolve, dirname, join, relative, sep, basename, extname } from 'path';
|
|
4
4
|
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
5
5
|
import { createHash } from 'crypto';
|
|
@@ -280,6 +280,23 @@ function todayIso() {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// src/lessons/add-errors.ts
|
|
283
|
+
var EmptyRuleError = class extends Error {
|
|
284
|
+
code = "EMPTY_RULE";
|
|
285
|
+
constructor() {
|
|
286
|
+
super("Lesson rule must not be empty \u2014 pass one imperative sentence.");
|
|
287
|
+
this.name = "EmptyRuleError";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var BroadCommandPatternError = class extends Error {
|
|
291
|
+
constructor(pattern) {
|
|
292
|
+
super(
|
|
293
|
+
`Command pattern ${JSON.stringify(pattern)} matches nearly every command, so it would fire on every recall. Key it on the action instead \u2014 a word-bounded program + subcommand (e.g. "\\bgit commit\\b", "\\brm\\b").`
|
|
294
|
+
);
|
|
295
|
+
this.pattern = pattern;
|
|
296
|
+
this.name = "BroadCommandPatternError";
|
|
297
|
+
}
|
|
298
|
+
code = "BROAD_COMMAND_PATTERN";
|
|
299
|
+
};
|
|
283
300
|
var UnknownTopicError = class extends Error {
|
|
284
301
|
constructor(topic) {
|
|
285
302
|
super(`Unknown topic: ${topic}. Pass allowNewTopic + topicSummary to create it.`);
|
|
@@ -319,992 +336,1144 @@ var UnrecallableLessonError = class extends Error {
|
|
|
319
336
|
code = "UNRECALLABLE_LESSON";
|
|
320
337
|
};
|
|
321
338
|
|
|
322
|
-
// src/lessons/
|
|
323
|
-
var
|
|
324
|
-
var
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"and",
|
|
333
|
-
"or",
|
|
334
|
-
"for",
|
|
335
|
-
"is",
|
|
336
|
-
"on",
|
|
337
|
-
"at",
|
|
338
|
-
"with",
|
|
339
|
-
"be",
|
|
340
|
-
"as",
|
|
341
|
-
"it",
|
|
342
|
-
"that",
|
|
343
|
-
"this",
|
|
344
|
-
"its",
|
|
345
|
-
"must"
|
|
346
|
-
]);
|
|
347
|
-
function tokenize(text) {
|
|
348
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
349
|
-
}
|
|
350
|
-
function queryTerms(query) {
|
|
351
|
-
const parts = [];
|
|
352
|
-
if (query.keyword !== void 0) parts.push(query.keyword);
|
|
353
|
-
if (query.file !== void 0) parts.push(query.file);
|
|
354
|
-
if (query.command !== void 0) parts.push(query.command);
|
|
355
|
-
return tokenize(parts.join(" "));
|
|
356
|
-
}
|
|
357
|
-
function buildCorpus(graph) {
|
|
358
|
-
const docs = [];
|
|
359
|
-
const df = /* @__PURE__ */ new Map();
|
|
360
|
-
let total = 0;
|
|
361
|
-
let n = 0;
|
|
362
|
-
for (const lesson of Object.values(graph.lessons)) {
|
|
363
|
-
if (lesson.status !== "active") continue;
|
|
364
|
-
const toks = tokenize(lesson.rule);
|
|
365
|
-
n += 1;
|
|
366
|
-
total += toks.length;
|
|
367
|
-
docs.push(toks.length);
|
|
368
|
-
for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
339
|
+
// src/lessons/regex-linear/nfa-compile.ts
|
|
340
|
+
var MAX_NFA_STATES = 2e3;
|
|
341
|
+
var Builder = class {
|
|
342
|
+
states = [];
|
|
343
|
+
alloc() {
|
|
344
|
+
if (this.states.length >= MAX_NFA_STATES) {
|
|
345
|
+
throw new Error(`NFA state limit exceeded (${MAX_NFA_STATES}); pattern expands too large`);
|
|
346
|
+
}
|
|
347
|
+
this.states.push({ eps: [], asserts: [], chars: [] });
|
|
348
|
+
return this.states.length - 1;
|
|
369
349
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return { idf, avgdl: total / N || 1 };
|
|
350
|
+
};
|
|
351
|
+
function isNonLineTerminator(c) {
|
|
352
|
+
return c !== "\n" && c !== "\r" && c !== "\u2028" && c !== "\u2029";
|
|
374
353
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
354
|
+
function compileNode(b, node) {
|
|
355
|
+
switch (node.k) {
|
|
356
|
+
case "empty":
|
|
357
|
+
case "assert": {
|
|
358
|
+
const s = b.alloc();
|
|
359
|
+
const e = b.alloc();
|
|
360
|
+
if (node.k === "assert") b.states[s].asserts.push({ kind: node.kind, target: e });
|
|
361
|
+
else b.states[s].eps.push(e);
|
|
362
|
+
return { start: s, end: e };
|
|
363
|
+
}
|
|
364
|
+
case "char":
|
|
365
|
+
case "any":
|
|
366
|
+
case "class": {
|
|
367
|
+
const s = b.alloc();
|
|
368
|
+
const e = b.alloc();
|
|
369
|
+
const test = node.k === "char" ? (c) => c === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
|
|
370
|
+
b.states[s].chars.push({ test, target: e });
|
|
371
|
+
return { start: s, end: e };
|
|
372
|
+
}
|
|
373
|
+
case "concat": {
|
|
374
|
+
if (node.items.length === 0) return compileNode(b, { k: "empty" });
|
|
375
|
+
let first = null;
|
|
376
|
+
let prevEnd = -1;
|
|
377
|
+
for (const item of node.items) {
|
|
378
|
+
const frag = compileNode(b, item);
|
|
379
|
+
if (first === null) first = frag;
|
|
380
|
+
else b.states[prevEnd].eps.push(frag.start);
|
|
381
|
+
prevEnd = frag.end;
|
|
382
|
+
}
|
|
383
|
+
return { start: first.start, end: prevEnd };
|
|
384
|
+
}
|
|
385
|
+
case "alt": {
|
|
386
|
+
const s = b.alloc();
|
|
387
|
+
const e = b.alloc();
|
|
388
|
+
for (const opt of node.opts) {
|
|
389
|
+
const frag = compileNode(b, opt);
|
|
390
|
+
b.states[s].eps.push(frag.start);
|
|
391
|
+
b.states[frag.end].eps.push(e);
|
|
392
|
+
}
|
|
393
|
+
return { start: s, end: e };
|
|
394
|
+
}
|
|
395
|
+
case "opt": {
|
|
396
|
+
const s = b.alloc();
|
|
397
|
+
const e = b.alloc();
|
|
398
|
+
const frag = compileNode(b, node.node);
|
|
399
|
+
b.states[s].eps.push(frag.start, e);
|
|
400
|
+
b.states[frag.end].eps.push(e);
|
|
401
|
+
return { start: s, end: e };
|
|
402
|
+
}
|
|
403
|
+
case "star": {
|
|
404
|
+
const s = b.alloc();
|
|
405
|
+
const e = b.alloc();
|
|
406
|
+
const frag = compileNode(b, node.node);
|
|
407
|
+
b.states[s].eps.push(frag.start, e);
|
|
408
|
+
b.states[frag.end].eps.push(frag.start, e);
|
|
409
|
+
return { start: s, end: e };
|
|
410
|
+
}
|
|
411
|
+
case "plus": {
|
|
412
|
+
const e = b.alloc();
|
|
413
|
+
const frag = compileNode(b, node.node);
|
|
414
|
+
b.states[frag.end].eps.push(frag.start, e);
|
|
415
|
+
return { start: frag.start, end: e };
|
|
416
|
+
}
|
|
386
417
|
}
|
|
387
|
-
|
|
418
|
+
}
|
|
419
|
+
function compileNfa(ast) {
|
|
420
|
+
const b = new Builder();
|
|
421
|
+
const { start, end } = compileNode(b, ast);
|
|
422
|
+
return { states: b.states, start, accept: end };
|
|
388
423
|
}
|
|
389
424
|
|
|
390
|
-
// src/lessons/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
425
|
+
// src/lessons/regex-linear/nfa.ts
|
|
426
|
+
function wordBoundary(input, pos) {
|
|
427
|
+
const before = pos > 0 && /[A-Za-z0-9_]/.test(input[pos - 1]);
|
|
428
|
+
const after = pos < input.length && /[A-Za-z0-9_]/.test(input[pos]);
|
|
429
|
+
return before !== after;
|
|
394
430
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
431
|
+
function assertHolds(kind, input, pos) {
|
|
432
|
+
switch (kind) {
|
|
433
|
+
case "start":
|
|
434
|
+
return pos === 0;
|
|
435
|
+
case "end":
|
|
436
|
+
return pos === input.length;
|
|
437
|
+
case "wordB":
|
|
438
|
+
return wordBoundary(input, pos);
|
|
439
|
+
case "nonWordB":
|
|
440
|
+
return !wordBoundary(input, pos);
|
|
441
|
+
}
|
|
397
442
|
}
|
|
398
|
-
function
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
443
|
+
function buildMatcher(ast) {
|
|
444
|
+
const { states, start, accept } = compileNfa(ast);
|
|
445
|
+
const closure = (set, idx, input, pos, budget) => {
|
|
446
|
+
const stack = [idx];
|
|
447
|
+
while (stack.length > 0) {
|
|
448
|
+
if (budget.remaining <= 0) return;
|
|
449
|
+
const cur = stack.pop();
|
|
450
|
+
if (set.has(cur)) continue;
|
|
451
|
+
set.add(cur);
|
|
452
|
+
budget.remaining -= 1;
|
|
453
|
+
for (const t of states[cur].eps) if (!set.has(t)) stack.push(t);
|
|
454
|
+
for (const a of states[cur].asserts) {
|
|
455
|
+
if (assertHolds(a.kind, input, pos) && !set.has(a.target)) stack.push(a.target);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
return {
|
|
460
|
+
// The input is matched in full (no truncation — truncation would miss suffix
|
|
461
|
+
// matches and let `$` falsely match an invented endpoint). Work is bounded by
|
|
462
|
+
// the shared budget instead: when it runs out we report a safe non-match.
|
|
463
|
+
test(input, budget) {
|
|
464
|
+
const b = budget ?? { remaining: Number.POSITIVE_INFINITY };
|
|
465
|
+
if (b.remaining <= 0) return false;
|
|
466
|
+
let current = /* @__PURE__ */ new Set();
|
|
467
|
+
for (let pos = 0; pos <= input.length; pos += 1) {
|
|
468
|
+
closure(current, start, input, pos, b);
|
|
469
|
+
if (current.has(accept)) return true;
|
|
470
|
+
if (b.remaining <= 0) return false;
|
|
471
|
+
if (pos === input.length) break;
|
|
472
|
+
const ch = input[pos];
|
|
473
|
+
const next = /* @__PURE__ */ new Set();
|
|
474
|
+
for (const s of current) {
|
|
475
|
+
b.remaining -= 1;
|
|
476
|
+
for (const t of states[s].chars) {
|
|
477
|
+
if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
current = next;
|
|
481
|
+
}
|
|
482
|
+
return current.has(accept);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
402
485
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
486
|
+
|
|
487
|
+
// src/lessons/regex-linear/ast.ts
|
|
488
|
+
var UnsupportedRegexError = class extends Error {
|
|
489
|
+
constructor(message) {
|
|
490
|
+
super(message);
|
|
491
|
+
this.name = "UnsupportedRegexError";
|
|
408
492
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// src/lessons/regex-linear/parse-helpers.ts
|
|
496
|
+
var MAX_REPEAT = 1e3;
|
|
497
|
+
var isWord = (c) => /[A-Za-z0-9_]/.test(c);
|
|
498
|
+
function expandRepeat(atom, min, max) {
|
|
499
|
+
const items = [];
|
|
500
|
+
for (let k = 0; k < min; k += 1) items.push(atom);
|
|
501
|
+
if (max === Infinity) {
|
|
502
|
+
items.push({ k: "star", node: atom });
|
|
503
|
+
} else {
|
|
504
|
+
for (let k = min; k < max; k += 1) items.push({ k: "opt", node: atom });
|
|
420
505
|
}
|
|
421
|
-
return
|
|
506
|
+
if (items.length === 0) return { k: "empty" };
|
|
507
|
+
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
422
508
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
509
|
+
function escapeClass(c) {
|
|
510
|
+
switch (c) {
|
|
511
|
+
case "d":
|
|
512
|
+
return (x) => x >= "0" && x <= "9";
|
|
513
|
+
case "D":
|
|
514
|
+
return (x) => !(x >= "0" && x <= "9");
|
|
515
|
+
case "w":
|
|
516
|
+
return isWord;
|
|
517
|
+
case "W":
|
|
518
|
+
return (x) => !isWord(x);
|
|
519
|
+
case "s":
|
|
520
|
+
return (x) => /\s/.test(x);
|
|
521
|
+
case "S":
|
|
522
|
+
return (x) => !/\s/.test(x);
|
|
523
|
+
default:
|
|
524
|
+
return null;
|
|
431
525
|
}
|
|
432
526
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
527
|
+
var HEX2 = /^[0-9a-fA-F]{2}$/;
|
|
528
|
+
var HEX4 = /^[0-9a-fA-F]{4}$/;
|
|
529
|
+
function readUnicodeEscape(src, i, c) {
|
|
530
|
+
if (c === "x") {
|
|
531
|
+
const hex2 = src.slice(i, i + 2);
|
|
532
|
+
return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
|
|
533
|
+
}
|
|
534
|
+
const hex = src.slice(i, i + 4);
|
|
535
|
+
return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
|
|
438
536
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (trigger.kind !== "command_pattern") continue;
|
|
444
|
-
if (!active.has(triggerId)) continue;
|
|
445
|
-
if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
|
|
446
|
-
findings.push({
|
|
447
|
-
level: "warning",
|
|
448
|
-
code: "RUNNER_ANCHORED_PATTERN",
|
|
449
|
-
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\`).`,
|
|
450
|
-
triggerId
|
|
451
|
-
});
|
|
537
|
+
function readControlEscape(src, i) {
|
|
538
|
+
const x = src[i];
|
|
539
|
+
if (x === void 0 || !/[A-Za-z]/.test(x)) {
|
|
540
|
+
throw new UnsupportedRegexError("\\c must be followed by a letter");
|
|
452
541
|
}
|
|
542
|
+
return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
|
|
453
543
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const p = pattern.trim();
|
|
460
|
-
if (p === "*" || p === "**") return true;
|
|
461
|
-
if (!p.includes("**")) return false;
|
|
462
|
-
const basename5 = p.slice(p.lastIndexOf("/") + 1);
|
|
463
|
-
return basename5.startsWith("*");
|
|
544
|
+
function classEscapeChar(src, i, e) {
|
|
545
|
+
if (e === "b") return { ch: "\b", len: 0 };
|
|
546
|
+
if (e === "c") return readControlEscape(src, i);
|
|
547
|
+
if (e === "x" || e === "u") return readUnicodeEscape(src, i, e);
|
|
548
|
+
return { ch: escapeLiteral(e), len: 0 };
|
|
464
549
|
}
|
|
465
|
-
function
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
550
|
+
function escapeLiteral(c) {
|
|
551
|
+
switch (c) {
|
|
552
|
+
case "t":
|
|
553
|
+
return " ";
|
|
554
|
+
case "n":
|
|
555
|
+
return "\n";
|
|
556
|
+
case "r":
|
|
557
|
+
return "\r";
|
|
558
|
+
case "f":
|
|
559
|
+
return "\f";
|
|
560
|
+
case "v":
|
|
561
|
+
return "\v";
|
|
562
|
+
case "0":
|
|
563
|
+
return "\0";
|
|
564
|
+
default:
|
|
565
|
+
return c;
|
|
474
566
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/lessons/regex-linear/parse.ts
|
|
570
|
+
function parseRegex(src) {
|
|
571
|
+
let i = 0;
|
|
572
|
+
const peek = () => src[i];
|
|
573
|
+
const eat = () => src[i++];
|
|
574
|
+
function parseAlt() {
|
|
575
|
+
const opts = [parseConcat()];
|
|
576
|
+
while (peek() === "|") {
|
|
577
|
+
i += 1;
|
|
578
|
+
opts.push(parseConcat());
|
|
579
|
+
}
|
|
580
|
+
return opts.length === 1 ? opts[0] : { k: "alt", opts };
|
|
482
581
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
582
|
+
function parseConcat() {
|
|
583
|
+
const items = [];
|
|
584
|
+
while (i < src.length && peek() !== "|" && peek() !== ")") {
|
|
585
|
+
items.push(parseQuantified());
|
|
586
|
+
}
|
|
587
|
+
if (items.length === 0) return { k: "empty" };
|
|
588
|
+
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
488
589
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
590
|
+
function parseQuantified() {
|
|
591
|
+
const atom = parseAtom();
|
|
592
|
+
const q = peek();
|
|
593
|
+
if (q === "*" || q === "+" || q === "?") {
|
|
594
|
+
i += 1;
|
|
595
|
+
if (peek() === "?") i += 1;
|
|
596
|
+
return q === "*" ? { k: "star", node: atom } : q === "+" ? { k: "plus", node: atom } : { k: "opt", node: atom };
|
|
597
|
+
}
|
|
598
|
+
if (q === "{") {
|
|
599
|
+
const repeat = tryParseBrace();
|
|
600
|
+
if (repeat !== null) return expandRepeat(atom, repeat.min, repeat.max);
|
|
601
|
+
}
|
|
602
|
+
return atom;
|
|
495
603
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
604
|
+
function tryParseBrace() {
|
|
605
|
+
const m = /^\{(\d+)(,(\d*)?)?\}/.exec(src.slice(i));
|
|
606
|
+
if (m === null) return null;
|
|
607
|
+
i += m[0].length;
|
|
608
|
+
if (peek() === "?") i += 1;
|
|
609
|
+
const min = Number(m[1]);
|
|
610
|
+
const max = m[2] === void 0 ? min : m[3] === "" || m[3] === void 0 ? Infinity : Number(m[3]);
|
|
611
|
+
if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
|
|
612
|
+
throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
|
|
613
|
+
}
|
|
614
|
+
return { min, max };
|
|
502
615
|
}
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
616
|
+
function parseAtom() {
|
|
617
|
+
const c = peek();
|
|
618
|
+
if (c === "(") return parseGroup();
|
|
619
|
+
if (c === "[") return parseClass();
|
|
620
|
+
if (c === "\\") return parseEscape();
|
|
621
|
+
if (c === ".") {
|
|
622
|
+
i += 1;
|
|
623
|
+
return { k: "any" };
|
|
511
624
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
code: "WIDE_GLOB_MATCH",
|
|
516
|
-
message: `Lesson "${lessonId}" has file glob(s) (${wide.join(", ")}) matching more than ${WIDE_GLOB_MATCH_COUNT} files in the working tree; narrow to the file-CLASS where the rule actually applies so it does not fire on unrelated edits.`
|
|
517
|
-
});
|
|
625
|
+
if (c === "^") {
|
|
626
|
+
i += 1;
|
|
627
|
+
return { k: "assert", kind: "start" };
|
|
518
628
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
function nearDuplicateWarning(graph, lessonId) {
|
|
526
|
-
const subject = graph.lessons[lessonId];
|
|
527
|
-
if (subject === void 0) return null;
|
|
528
|
-
const subjectTokens = new Set(tokenize(subject.rule));
|
|
529
|
-
if (subjectTokens.size === 0) return null;
|
|
530
|
-
let best = null;
|
|
531
|
-
for (const [id, other] of Object.entries(graph.lessons)) {
|
|
532
|
-
if (id === lessonId || other.status !== "active") continue;
|
|
533
|
-
const otherTokens = new Set(tokenize(other.rule));
|
|
534
|
-
if (otherTokens.size === 0) continue;
|
|
535
|
-
const score = jaccard(subjectTokens, otherTokens);
|
|
536
|
-
if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
|
|
537
|
-
best = { id, score };
|
|
629
|
+
if (c === "$") {
|
|
630
|
+
i += 1;
|
|
631
|
+
return { k: "assert", kind: "end" };
|
|
632
|
+
}
|
|
633
|
+
if (c === void 0 || c === "*" || c === "+" || c === "?" || c === ")") {
|
|
634
|
+
throw new UnsupportedRegexError(`Unexpected '${c ?? "<end>"}' in pattern`);
|
|
538
635
|
}
|
|
636
|
+
i += 1;
|
|
637
|
+
return { k: "char", ch: c };
|
|
539
638
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
639
|
+
function parseGroup() {
|
|
640
|
+
i += 1;
|
|
641
|
+
if (peek() === "?") {
|
|
642
|
+
const c2 = src[i + 1];
|
|
643
|
+
if (c2 === "=" || c2 === "!" || c2 === "<") {
|
|
644
|
+
if (!(c2 === "<" && /[A-Za-z]/.test(src[i + 2] ?? ""))) {
|
|
645
|
+
throw new UnsupportedRegexError("Lookaround assertions are not supported");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (c2 === ":") i += 2;
|
|
649
|
+
else if (c2 === "<") {
|
|
650
|
+
i += 2;
|
|
651
|
+
while (i < src.length && src[i] !== ">") i += 1;
|
|
652
|
+
i += 1;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const inner = parseAlt();
|
|
656
|
+
if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
|
|
657
|
+
i += 1;
|
|
658
|
+
return inner;
|
|
559
659
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
660
|
+
function parseEscape() {
|
|
661
|
+
i += 1;
|
|
662
|
+
const c = peek();
|
|
663
|
+
if (c === void 0) throw new UnsupportedRegexError("Trailing backslash");
|
|
664
|
+
if (/[1-9]/.test(c) || c === "k")
|
|
665
|
+
throw new UnsupportedRegexError("Backreferences are not supported");
|
|
666
|
+
i += 1;
|
|
667
|
+
if (c === "b") return { k: "assert", kind: "wordB" };
|
|
668
|
+
if (c === "B") return { k: "assert", kind: "nonWordB" };
|
|
669
|
+
const cls = escapeClass(c);
|
|
670
|
+
if (cls !== null) return { k: "class", test: cls };
|
|
671
|
+
if (c === "x" || c === "u" || c === "c") {
|
|
672
|
+
const { ch, len } = c === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c);
|
|
673
|
+
i += len;
|
|
674
|
+
return { k: "char", ch };
|
|
675
|
+
}
|
|
676
|
+
return { k: "char", ch: escapeLiteral(c) };
|
|
577
677
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
678
|
+
function parseClass() {
|
|
679
|
+
i += 1;
|
|
680
|
+
const negate = peek() === "^";
|
|
681
|
+
if (negate) i += 1;
|
|
682
|
+
const tests = [];
|
|
683
|
+
while (i < src.length && peek() !== "]") {
|
|
684
|
+
tests.push(parseClassMember());
|
|
685
|
+
}
|
|
686
|
+
if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
|
|
687
|
+
i += 1;
|
|
688
|
+
const base = (c) => tests.some((t) => t(c));
|
|
689
|
+
return { k: "class", test: negate ? (c) => !base(c) : base };
|
|
587
690
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
let attempt = 0;
|
|
601
|
-
while (true) {
|
|
602
|
-
const acquired = await tryAcquire(lockPath);
|
|
603
|
-
if (acquired) return acquired;
|
|
604
|
-
const existing = await inspectLock(lockPath);
|
|
605
|
-
if (existing !== "young" && isStale(existing, stale)) {
|
|
606
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
607
|
-
});
|
|
608
|
-
continue;
|
|
691
|
+
function parseClassMember() {
|
|
692
|
+
let lo;
|
|
693
|
+
if (peek() === "\\") {
|
|
694
|
+
i += 1;
|
|
695
|
+
const e = eat();
|
|
696
|
+
const cls = escapeClass(e);
|
|
697
|
+
if (cls !== null) return cls;
|
|
698
|
+
const r = classEscapeChar(src, i, e);
|
|
699
|
+
i += r.len;
|
|
700
|
+
lo = r.ch;
|
|
701
|
+
} else {
|
|
702
|
+
lo = eat();
|
|
609
703
|
}
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
|
|
704
|
+
if (peek() === "-" && src[i + 1] !== void 0 && src[i + 1] !== "]") {
|
|
705
|
+
i += 1;
|
|
706
|
+
let hi;
|
|
707
|
+
if (peek() === "\\") {
|
|
708
|
+
i += 1;
|
|
709
|
+
const e2 = eat();
|
|
710
|
+
const r = classEscapeChar(src, i, e2);
|
|
711
|
+
i += r.len;
|
|
712
|
+
hi = r.ch;
|
|
713
|
+
} else {
|
|
714
|
+
hi = eat();
|
|
715
|
+
}
|
|
716
|
+
const a = lo.codePointAt(0);
|
|
717
|
+
const b = hi.codePointAt(0);
|
|
718
|
+
return (c) => {
|
|
719
|
+
const p = c.codePointAt(0);
|
|
720
|
+
return p >= a && p <= b;
|
|
721
|
+
};
|
|
613
722
|
}
|
|
614
|
-
|
|
615
|
-
await sleep(delay);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
async function tryAcquire(lockPath) {
|
|
619
|
-
try {
|
|
620
|
-
await mkdir(lockPath, { recursive: false });
|
|
621
|
-
} catch (err) {
|
|
622
|
-
if (err.code === "EEXIST") return null;
|
|
623
|
-
throw err;
|
|
723
|
+
return (c) => c === lo;
|
|
624
724
|
}
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
started: Date.now(),
|
|
629
|
-
hostname: getHostname()
|
|
630
|
-
};
|
|
631
|
-
await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
632
|
-
let released = false;
|
|
633
|
-
const cleanup = () => {
|
|
634
|
-
if (released) return;
|
|
635
|
-
released = true;
|
|
636
|
-
try {
|
|
637
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
638
|
-
} catch {
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
const signalHandler = (signal) => {
|
|
642
|
-
cleanup();
|
|
643
|
-
process.kill(process.pid, signal);
|
|
644
|
-
};
|
|
645
|
-
process.once("SIGINT", signalHandler);
|
|
646
|
-
process.once("SIGTERM", signalHandler);
|
|
647
|
-
process.once("exit", cleanup);
|
|
648
|
-
return async () => {
|
|
649
|
-
if (released) return;
|
|
650
|
-
released = true;
|
|
651
|
-
process.off("SIGINT", signalHandler);
|
|
652
|
-
process.off("SIGTERM", signalHandler);
|
|
653
|
-
process.off("exit", cleanup);
|
|
654
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
655
|
-
});
|
|
656
|
-
};
|
|
725
|
+
const ast = parseAlt();
|
|
726
|
+
if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
|
|
727
|
+
return ast;
|
|
657
728
|
}
|
|
658
|
-
|
|
729
|
+
|
|
730
|
+
// src/lessons/regex-linear/index.ts
|
|
731
|
+
var cache = /* @__PURE__ */ new Map();
|
|
732
|
+
function compileLinearMatcher(pattern) {
|
|
733
|
+
const hit = cache.get(pattern);
|
|
734
|
+
if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
|
|
735
|
+
let matcher;
|
|
659
736
|
try {
|
|
660
|
-
|
|
661
|
-
const parsed = JSON.parse(raw);
|
|
662
|
-
if (!isLockMetadata(parsed)) return null;
|
|
663
|
-
return parsed;
|
|
737
|
+
matcher = buildMatcher(parseRegex(pattern));
|
|
664
738
|
} catch {
|
|
665
|
-
|
|
666
|
-
const info = await stat(lockPath);
|
|
667
|
-
const ageMs = Date.now() - info.mtimeMs;
|
|
668
|
-
if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
|
|
669
|
-
} catch {
|
|
670
|
-
}
|
|
671
|
-
return null;
|
|
739
|
+
matcher = null;
|
|
672
740
|
}
|
|
741
|
+
cache.set(pattern, matcher);
|
|
742
|
+
return matcher;
|
|
673
743
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
return
|
|
744
|
+
|
|
745
|
+
// src/lessons/regex-safety.ts
|
|
746
|
+
var MAX_PATTERN_LENGTH = 1e3;
|
|
747
|
+
function isSafeRegexPattern(pattern) {
|
|
748
|
+
if (pattern.length > MAX_PATTERN_LENGTH) return false;
|
|
749
|
+
return compileLinearMatcher(pattern) !== null;
|
|
680
750
|
}
|
|
681
|
-
function
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
751
|
+
function getCommandMatcher(pattern) {
|
|
752
|
+
if (pattern.length > MAX_PATTERN_LENGTH) return null;
|
|
753
|
+
return compileLinearMatcher(pattern);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/lessons/command-pattern-breadth.ts
|
|
757
|
+
var COMMAND_PROBE_CORPUS = [
|
|
758
|
+
"git status",
|
|
759
|
+
'git commit -m "wip"',
|
|
760
|
+
"pnpm test",
|
|
761
|
+
"npx vitest run src/x.test.ts",
|
|
762
|
+
"ls -la",
|
|
763
|
+
"cat README.md",
|
|
764
|
+
"rm -rf dist",
|
|
765
|
+
"mkdir -p build/out",
|
|
766
|
+
"node scripts/build.js",
|
|
767
|
+
"docker compose up -d",
|
|
768
|
+
"curl -s https://example.com",
|
|
769
|
+
"echo hello > out.txt",
|
|
770
|
+
"sed -i 's/a/b/' file.txt",
|
|
771
|
+
"pnpm lint --fix",
|
|
772
|
+
"python3 -m pytest",
|
|
773
|
+
"cargo build --release",
|
|
774
|
+
"make",
|
|
775
|
+
"npm install --global typescript",
|
|
776
|
+
"cp a.txt b.txt",
|
|
777
|
+
"grep -rn TODO src"
|
|
778
|
+
];
|
|
779
|
+
var BROAD_HIT_RATIO = 0.5;
|
|
780
|
+
var PROBE_BUDGET = 1e5;
|
|
781
|
+
function isBroadCommandPattern(pattern) {
|
|
782
|
+
const matcher = getCommandMatcher(pattern);
|
|
783
|
+
if (matcher === null) return false;
|
|
784
|
+
if (matcher.test("", { remaining: PROBE_BUDGET })) return true;
|
|
785
|
+
let hits = 0;
|
|
786
|
+
for (const command of COMMAND_PROBE_CORPUS) {
|
|
787
|
+
if (matcher.test(command, { remaining: PROBE_BUDGET })) hits += 1;
|
|
688
788
|
}
|
|
789
|
+
return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
|
|
689
790
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
791
|
+
|
|
792
|
+
// src/lessons/ranking-text.ts
|
|
793
|
+
var K1 = 1.5;
|
|
794
|
+
var B = 0.75;
|
|
795
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
796
|
+
"the",
|
|
797
|
+
"a",
|
|
798
|
+
"an",
|
|
799
|
+
"to",
|
|
800
|
+
"of",
|
|
801
|
+
"in",
|
|
802
|
+
"and",
|
|
803
|
+
"or",
|
|
804
|
+
"for",
|
|
805
|
+
"is",
|
|
806
|
+
"on",
|
|
807
|
+
"at",
|
|
808
|
+
"with",
|
|
809
|
+
"be",
|
|
810
|
+
"as",
|
|
811
|
+
"it",
|
|
812
|
+
"that",
|
|
813
|
+
"this",
|
|
814
|
+
"its",
|
|
815
|
+
"must"
|
|
816
|
+
]);
|
|
817
|
+
function tokenize(text) {
|
|
818
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
694
819
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
820
|
+
function queryTerms(query) {
|
|
821
|
+
const parts = [];
|
|
822
|
+
if (query.keyword !== void 0) parts.push(query.keyword);
|
|
823
|
+
if (query.file !== void 0) parts.push(query.file);
|
|
824
|
+
if (query.command !== void 0) parts.push(query.command);
|
|
825
|
+
return tokenize(parts.join(" "));
|
|
699
826
|
}
|
|
700
|
-
function
|
|
701
|
-
|
|
827
|
+
function buildCorpus(graph) {
|
|
828
|
+
const docs = [];
|
|
829
|
+
const df = /* @__PURE__ */ new Map();
|
|
830
|
+
let total = 0;
|
|
831
|
+
let n = 0;
|
|
832
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
833
|
+
if (lesson.status !== "active") continue;
|
|
834
|
+
const toks = tokenize(lesson.rule);
|
|
835
|
+
n += 1;
|
|
836
|
+
total += toks.length;
|
|
837
|
+
docs.push(toks.length);
|
|
838
|
+
for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
839
|
+
}
|
|
840
|
+
const N = Math.max(n, 1);
|
|
841
|
+
const idf = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
|
|
843
|
+
return { idf, avgdl: total / N || 1 };
|
|
702
844
|
}
|
|
703
|
-
function
|
|
704
|
-
|
|
845
|
+
function bm25(terms, ruleText, corpus) {
|
|
846
|
+
const toks = tokenize(ruleText);
|
|
847
|
+
const dl = toks.length || 1;
|
|
848
|
+
const tf = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
850
|
+
let score = 0;
|
|
851
|
+
for (const t of new Set(terms)) {
|
|
852
|
+
const f = tf.get(t) ?? 0;
|
|
853
|
+
if (f === 0) continue;
|
|
854
|
+
const idf = corpus.idf.get(t);
|
|
855
|
+
score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
|
|
856
|
+
}
|
|
857
|
+
return score;
|
|
705
858
|
}
|
|
706
859
|
|
|
707
|
-
// src/lessons/
|
|
708
|
-
var
|
|
709
|
-
function
|
|
710
|
-
return
|
|
860
|
+
// src/lessons/keyword-signal.ts
|
|
861
|
+
var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
|
|
862
|
+
function isLowSignalKeyword(pattern) {
|
|
863
|
+
return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
|
|
711
864
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
865
|
+
function splitRawTokens(pattern) {
|
|
866
|
+
return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
867
|
+
}
|
|
868
|
+
function keywordNeedleLosesTokens(pattern) {
|
|
869
|
+
const raw = splitRawTokens(pattern);
|
|
870
|
+
if (raw.length < 2) return false;
|
|
871
|
+
return tokenize(pattern).length !== raw.length;
|
|
716
872
|
}
|
|
717
873
|
|
|
718
|
-
// src/lessons/
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
code: "DANGLING_TRIGGER",
|
|
737
|
-
message: `Lesson "${lessonId}" references unknown trigger "${triggerId}".`,
|
|
738
|
-
lessonId,
|
|
739
|
-
triggerId
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (lesson.supersededBy !== void 0 && graph.lessons[lesson.supersededBy] === void 0) {
|
|
744
|
-
findings.push({
|
|
745
|
-
level: "error",
|
|
746
|
-
code: "DANGLING_SUPERSEDER",
|
|
747
|
-
message: `Lesson "${lessonId}" supersededBy unknown lesson "${lesson.supersededBy}".`,
|
|
748
|
-
lessonId
|
|
749
|
-
});
|
|
874
|
+
// src/lessons/trigger-effectiveness.ts
|
|
875
|
+
function ineffectiveTriggers(graph, triggerIds) {
|
|
876
|
+
const out = [];
|
|
877
|
+
for (const id of triggerIds) {
|
|
878
|
+
const trigger = graph.triggers[id];
|
|
879
|
+
if (trigger === void 0) continue;
|
|
880
|
+
const reason = ineffectiveReason(trigger.kind, trigger.pattern);
|
|
881
|
+
if (reason !== null) out.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
function ineffectiveReason(kind, pattern) {
|
|
886
|
+
if (kind === "keyword") {
|
|
887
|
+
if (tokenize(pattern).length === 0) {
|
|
888
|
+
return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
|
|
889
|
+
}
|
|
890
|
+
if (keywordNeedleLosesTokens(pattern)) {
|
|
891
|
+
return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
|
|
750
892
|
}
|
|
893
|
+
return null;
|
|
751
894
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
code: "DUPLICATE_TOPIC_REF",
|
|
759
|
-
message: `Lesson "${lessonId}" references topic "${topicId}" more than once.`,
|
|
760
|
-
lessonId,
|
|
761
|
-
topicId
|
|
762
|
-
});
|
|
895
|
+
if (kind === "command_pattern") {
|
|
896
|
+
let valid = true;
|
|
897
|
+
try {
|
|
898
|
+
new RegExp(pattern);
|
|
899
|
+
} catch {
|
|
900
|
+
valid = false;
|
|
763
901
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
lessonId,
|
|
770
|
-
triggerId
|
|
771
|
-
});
|
|
902
|
+
if (!valid) {
|
|
903
|
+
return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
|
|
904
|
+
}
|
|
905
|
+
if (!isSafeRegexPattern(pattern)) {
|
|
906
|
+
return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
|
|
772
907
|
}
|
|
908
|
+
return null;
|
|
773
909
|
}
|
|
910
|
+
return null;
|
|
774
911
|
}
|
|
775
|
-
function
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
912
|
+
function blockingDeadTriggers(graph, triggerIds) {
|
|
913
|
+
return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/lessons/add-gates.ts
|
|
917
|
+
function assertRuleShape(rule) {
|
|
918
|
+
const trimmed = rule.trim();
|
|
919
|
+
if (trimmed.length === 0) throw new EmptyRuleError();
|
|
920
|
+
if (trimmed.length > MAX_RULE_LENGTH) throw new RuleTooLongError(trimmed.length, MAX_RULE_LENGTH);
|
|
921
|
+
return trimmed;
|
|
922
|
+
}
|
|
923
|
+
function skipsTriggerGates(input, options) {
|
|
924
|
+
return options.allowNoTrigger === true || input.scope === "always";
|
|
925
|
+
}
|
|
926
|
+
function countInputTriggers(triggers) {
|
|
927
|
+
return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
|
|
928
|
+
}
|
|
929
|
+
function assertTriggerInputs(input, options, existingTriggerCount) {
|
|
930
|
+
if (!skipsTriggerGates(input, options) && countInputTriggers(input.triggers) === 0 && existingTriggerCount === 0) {
|
|
931
|
+
throw new NoTriggerError();
|
|
932
|
+
}
|
|
933
|
+
if (options.allowNoTrigger !== true) {
|
|
934
|
+
const broad = (input.triggers.commands ?? []).find(isBroadCommandPattern);
|
|
935
|
+
if (broad !== void 0) throw new BroadCommandPatternError(broad);
|
|
781
936
|
}
|
|
782
|
-
return [...dup];
|
|
783
937
|
}
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
level: "error",
|
|
789
|
-
code: "SUPERSEDED_WITHOUT_TARGET",
|
|
790
|
-
message: `Lesson "${lessonId}" has status "superseded" but no supersededBy target.`,
|
|
791
|
-
lessonId
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
if (lesson.status === "active" && lesson.supersededBy !== void 0) {
|
|
795
|
-
findings.push({
|
|
796
|
-
level: "error",
|
|
797
|
-
code: "ACTIVE_WITH_SUPERSEDER",
|
|
798
|
-
message: `Lesson "${lessonId}" has status "active" but declares supersededBy.`,
|
|
799
|
-
lessonId
|
|
800
|
-
});
|
|
801
|
-
}
|
|
938
|
+
function assertRecallable(graph, resultingTriggers) {
|
|
939
|
+
const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
|
|
940
|
+
if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
|
|
941
|
+
throw new UnrecallableLessonError(blockingDead);
|
|
802
942
|
}
|
|
803
943
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
const target = graph.lessons[lesson.supersededBy];
|
|
817
|
-
if (target !== void 0 && target.status !== "active") {
|
|
818
|
-
findings.push({
|
|
819
|
-
level: "error",
|
|
820
|
-
code: "INACTIVE_SUPERSEDER",
|
|
821
|
-
message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target.status} \u2014 the chain dead-ends with no live replacement.`,
|
|
822
|
-
lessonId
|
|
823
|
-
});
|
|
824
|
-
}
|
|
944
|
+
|
|
945
|
+
// src/lessons/glob-breadth.ts
|
|
946
|
+
var WILDCARD = /[*?[\]]/;
|
|
947
|
+
function globNarrowness(pattern) {
|
|
948
|
+
const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
949
|
+
if (segments.length === 0) return 0;
|
|
950
|
+
let literal = 0;
|
|
951
|
+
let globstars = 0;
|
|
952
|
+
for (const segment of segments) {
|
|
953
|
+
if (segment === "**") globstars += 1;
|
|
954
|
+
else if (!WILDCARD.test(segment)) literal += 1;
|
|
825
955
|
}
|
|
826
|
-
|
|
956
|
+
return literal / (segments.length + globstars);
|
|
827
957
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
findings.push({
|
|
838
|
-
level: "error",
|
|
839
|
-
code: "SUPERSEDE_CYCLE",
|
|
840
|
-
message: `Lesson "${startId}" is part of a supersededBy cycle.`,
|
|
841
|
-
lessonId: startId
|
|
842
|
-
});
|
|
843
|
-
break;
|
|
844
|
-
}
|
|
845
|
-
seen.add(cur);
|
|
846
|
-
const next = graph.lessons[cur]?.supersededBy;
|
|
847
|
-
if (next === cur) break;
|
|
848
|
-
cur = next;
|
|
849
|
-
}
|
|
958
|
+
var BROAD_GLOB_NARROWNESS = 0.34;
|
|
959
|
+
function isBroadFileGlob(pattern) {
|
|
960
|
+
return globNarrowness(pattern) < BROAD_GLOB_NARROWNESS;
|
|
961
|
+
}
|
|
962
|
+
function activeTriggerIds(graph) {
|
|
963
|
+
const ids = /* @__PURE__ */ new Set();
|
|
964
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
965
|
+
if (lesson.status !== "active") continue;
|
|
966
|
+
for (const t of lesson.triggers) ids.add(t);
|
|
850
967
|
}
|
|
968
|
+
return ids;
|
|
851
969
|
}
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
970
|
+
function deadFileGlobIds(graph, knownPaths) {
|
|
971
|
+
const active = activeTriggerIds(graph);
|
|
972
|
+
const paths = [...knownPaths];
|
|
973
|
+
const dead = /* @__PURE__ */ new Set();
|
|
974
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
975
|
+
if (trigger.kind !== "file_glob") continue;
|
|
976
|
+
if (!active.has(triggerId)) continue;
|
|
977
|
+
const isMatch = picomatch(trigger.pattern, { dot: true });
|
|
978
|
+
if (!paths.some((p) => isMatch(p))) dead.add(triggerId);
|
|
862
979
|
}
|
|
980
|
+
return dead;
|
|
863
981
|
}
|
|
864
|
-
function
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
982
|
+
function collectDeadFileGlobs(graph, findings, knownPaths) {
|
|
983
|
+
for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
|
|
984
|
+
findings.push({
|
|
985
|
+
level: "warning",
|
|
986
|
+
code: "DEAD_FILE_GLOB",
|
|
987
|
+
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\`.`,
|
|
988
|
+
triggerId
|
|
989
|
+
});
|
|
870
990
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
991
|
+
}
|
|
992
|
+
function fileGlobMatchCount(pattern, knownPaths) {
|
|
993
|
+
const isMatch = picomatch(pattern, { dot: true });
|
|
994
|
+
let n = 0;
|
|
995
|
+
for (const p of knownPaths) if (isMatch(p)) n += 1;
|
|
996
|
+
return n;
|
|
997
|
+
}
|
|
998
|
+
var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
|
|
999
|
+
function collectRunnerAnchoredPatterns(graph, findings) {
|
|
1000
|
+
const active = activeTriggerIds(graph);
|
|
1001
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1002
|
+
if (trigger.kind !== "command_pattern") continue;
|
|
1003
|
+
if (!active.has(triggerId)) continue;
|
|
1004
|
+
if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
|
|
1005
|
+
findings.push({
|
|
1006
|
+
level: "warning",
|
|
1007
|
+
code: "RUNNER_ANCHORED_PATTERN",
|
|
1008
|
+
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\`).`,
|
|
1009
|
+
triggerId
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function collectBroadFileGlobs(graph, findings) {
|
|
1014
|
+
const active = activeTriggerIds(graph);
|
|
1015
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1016
|
+
if (trigger.kind !== "file_glob") continue;
|
|
1017
|
+
if (!active.has(triggerId)) continue;
|
|
1018
|
+
if (!isBroadFileGlob(trigger.pattern)) continue;
|
|
1019
|
+
findings.push({
|
|
1020
|
+
level: "warning",
|
|
1021
|
+
code: "BROAD_FILE_GLOB",
|
|
1022
|
+
message: `file_glob trigger "${triggerId}" (${trigger.pattern}) matches most of the repository, so it outranks nothing and crowds the recall budget. Narrow it to the directory or file class the rule is really about, or detach it with \`lessons untrigger\`.`,
|
|
1023
|
+
triggerId
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function collectBroadCommandPatterns(graph, findings) {
|
|
1028
|
+
const active = activeTriggerIds(graph);
|
|
1029
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1030
|
+
if (trigger.kind !== "command_pattern") continue;
|
|
1031
|
+
if (!active.has(triggerId)) continue;
|
|
1032
|
+
if (!isBroadCommandPattern(trigger.pattern)) continue;
|
|
1033
|
+
findings.push({
|
|
1034
|
+
level: "warning",
|
|
1035
|
+
code: "BROAD_COMMAND_PATTERN",
|
|
1036
|
+
message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) matches nearly every command, so the lesson fires on every recall. Key it on the action (e.g. \`\\bgit commit\\b\`), or detach it with \`lessons untrigger\`.`,
|
|
1037
|
+
triggerId
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/lessons/capture-guardrails.ts
|
|
1043
|
+
var WIDE_GLOB_MATCH_COUNT = 40;
|
|
1044
|
+
var MAX_RECOMMENDED_TRIGGERS = 8;
|
|
1045
|
+
function isBroadGlob(pattern) {
|
|
1046
|
+
const p = pattern.trim();
|
|
1047
|
+
if (p === "*" || p === "**") return true;
|
|
1048
|
+
if (!p.includes("**")) return false;
|
|
1049
|
+
const basename5 = p.slice(p.lastIndexOf("/") + 1);
|
|
1050
|
+
return basename5.startsWith("*");
|
|
1051
|
+
}
|
|
1052
|
+
function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
1053
|
+
const lesson = graph.lessons[lessonId];
|
|
1054
|
+
if (lesson === void 0) return [];
|
|
1055
|
+
const warnings = [];
|
|
1056
|
+
if (lesson.triggers.length > MAX_RECOMMENDED_TRIGGERS) {
|
|
1057
|
+
warnings.push({
|
|
1058
|
+
code: "OVERSIZED_LESSON_TRIGGERS",
|
|
1059
|
+
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.`
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
const triggers = lesson.triggers.map((id) => graph.triggers[id]).filter((t) => t !== void 0);
|
|
1063
|
+
const broad = triggers.filter((t) => t.kind === "file_glob" && isBroadGlob(t.pattern)).map((t) => t.pattern);
|
|
1064
|
+
if (broad.length > 0) {
|
|
1065
|
+
warnings.push({
|
|
1066
|
+
code: "BROAD_GLOB_TRIGGER",
|
|
1067
|
+
message: `Lesson "${lessonId}" has broad file glob(s) (${broad.join(", ")}) that match large swaths of the tree; prefer a path specific to the lesson.`
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (triggers.length > 0 && triggers.every((t) => t.kind === "keyword")) {
|
|
1071
|
+
warnings.push({
|
|
1072
|
+
code: "KEYWORD_ONLY_LESSON",
|
|
1073
|
+
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.`
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
const lowSignal = triggers.filter((t) => t.kind === "keyword" && isLowSignalKeyword(t.pattern)).map((t) => t.pattern);
|
|
1077
|
+
if (lowSignal.length > 0) {
|
|
1078
|
+
warnings.push({
|
|
1079
|
+
code: "LOW_SIGNAL_KEYWORD",
|
|
1080
|
+
message: `Lesson "${lessonId}" has long keyword trigger(s) (${lowSignal.join(", ")}); recall matches a keyword only as a contiguous token-run in --keyword or the file/command, so a pattern past ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens rarely fires \u2014 use a short distinctive phrase.`
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const stopworded = triggers.filter((t) => t.kind === "keyword" && keywordNeedleLosesTokens(t.pattern)).map((t) => t.pattern);
|
|
1084
|
+
if (stopworded.length > 0) {
|
|
1085
|
+
warnings.push({
|
|
1086
|
+
code: "STOPWORD_KEYWORD",
|
|
1087
|
+
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").`
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (knownPaths !== void 0) {
|
|
1091
|
+
const dead = deadFileGlobIds(graph, knownPaths);
|
|
1092
|
+
const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
|
|
1093
|
+
if (deadHere.length > 0) {
|
|
1094
|
+
warnings.push({
|
|
1095
|
+
code: "DEAD_GLOB",
|
|
1096
|
+
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.`
|
|
878
1097
|
});
|
|
879
1098
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
code: "ORPHAN_TRIGGER",
|
|
886
|
-
message: `Trigger "${triggerId}" is not referenced by any lesson.`,
|
|
887
|
-
triggerId
|
|
1099
|
+
const wide = triggers.filter((t) => t.kind === "file_glob" && !isBroadGlob(t.pattern)).filter((t) => fileGlobMatchCount(t.pattern, knownPaths) > WIDE_GLOB_MATCH_COUNT).map((t) => t.pattern);
|
|
1100
|
+
if (wide.length > 0) {
|
|
1101
|
+
warnings.push({
|
|
1102
|
+
code: "WIDE_GLOB_MATCH",
|
|
1103
|
+
message: `Lesson "${lessonId}" has file glob(s) (${wide.join(", ")}) matching more than ${WIDE_GLOB_MATCH_COUNT} files in the working tree; narrow to the file-CLASS where the rule actually applies so it does not fire on unrelated edits.`
|
|
888
1104
|
});
|
|
889
1105
|
}
|
|
890
1106
|
}
|
|
1107
|
+
return warnings;
|
|
891
1108
|
}
|
|
892
1109
|
|
|
893
|
-
// src/lessons/
|
|
894
|
-
var
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1110
|
+
// src/lessons/capture-near-duplicate.ts
|
|
1111
|
+
var NEAR_DUPLICATE_THRESHOLD = 0.6;
|
|
1112
|
+
function nearDuplicateWarning(graph, lessonId) {
|
|
1113
|
+
const subject = graph.lessons[lessonId];
|
|
1114
|
+
if (subject === void 0) return null;
|
|
1115
|
+
const subjectTokens = new Set(tokenize(subject.rule));
|
|
1116
|
+
if (subjectTokens.size === 0) return null;
|
|
1117
|
+
let best = null;
|
|
1118
|
+
for (const [id, other] of Object.entries(graph.lessons)) {
|
|
1119
|
+
if (id === lessonId || other.status !== "active") continue;
|
|
1120
|
+
const otherTokens = new Set(tokenize(other.rule));
|
|
1121
|
+
if (otherTokens.size === 0) continue;
|
|
1122
|
+
const score = jaccard(subjectTokens, otherTokens);
|
|
1123
|
+
if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
|
|
1124
|
+
best = { id, score };
|
|
900
1125
|
}
|
|
901
|
-
this.states.push({ eps: [], asserts: [], chars: [] });
|
|
902
|
-
return this.states.length - 1;
|
|
903
1126
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1127
|
+
if (best === null) return null;
|
|
1128
|
+
return {
|
|
1129
|
+
code: "NEAR_DUPLICATE_LESSON",
|
|
1130
|
+
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).`
|
|
1131
|
+
};
|
|
907
1132
|
}
|
|
908
|
-
function
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1133
|
+
function jaccard(a, b) {
|
|
1134
|
+
let intersection = 0;
|
|
1135
|
+
for (const t of a) if (b.has(t)) intersection += 1;
|
|
1136
|
+
return intersection / (a.size + b.size - intersection);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/core/errors.ts
|
|
1140
|
+
var AgentsMeshError = class extends Error {
|
|
1141
|
+
code;
|
|
1142
|
+
constructor(code, message, options) {
|
|
1143
|
+
super(message, options);
|
|
1144
|
+
this.name = "AgentsMeshError";
|
|
1145
|
+
this.code = code;
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
var LockAcquisitionError = class extends AgentsMeshError {
|
|
1149
|
+
lockPath;
|
|
1150
|
+
holder;
|
|
1151
|
+
/** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
|
|
1152
|
+
label;
|
|
1153
|
+
constructor(lockPath, holder, options) {
|
|
1154
|
+
const label = options?.label ?? "lock";
|
|
1155
|
+
super(
|
|
1156
|
+
"AM_LOCK_ACQUISITION_FAILED",
|
|
1157
|
+
`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.`,
|
|
1158
|
+
options
|
|
1159
|
+
);
|
|
1160
|
+
this.name = "LockAcquisitionError";
|
|
1161
|
+
this.lockPath = lockPath;
|
|
1162
|
+
this.holder = holder;
|
|
1163
|
+
this.label = label;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
var FileSystemError = class extends AgentsMeshError {
|
|
1167
|
+
path;
|
|
1168
|
+
errnoCode;
|
|
1169
|
+
constructor(path, message, options) {
|
|
1170
|
+
super("AM_FILESYSTEM", message, options);
|
|
1171
|
+
this.name = "FileSystemError";
|
|
1172
|
+
this.path = path;
|
|
1173
|
+
this.errnoCode = options?.errnoCode;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// src/utils/filesystem/process-lock.ts
|
|
1178
|
+
var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
1179
|
+
var DEFAULT_RETRIES = 30;
|
|
1180
|
+
var DEFAULT_RETRY_DELAY_MS = 200;
|
|
1181
|
+
var YOUNG_LOCK_GRACE_MS = 2e3;
|
|
1182
|
+
async function acquireProcessLock(lockPath, opts = {}) {
|
|
1183
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
1184
|
+
const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
1185
|
+
const stale = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
1186
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
1187
|
+
let attempt = 0;
|
|
1188
|
+
while (true) {
|
|
1189
|
+
const acquired = await tryAcquire(lockPath);
|
|
1190
|
+
if (acquired) return acquired;
|
|
1191
|
+
const existing = await inspectLock(lockPath);
|
|
1192
|
+
if (existing !== "young" && isStale(existing, stale)) {
|
|
1193
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
1194
|
+
});
|
|
1195
|
+
continue;
|
|
964
1196
|
}
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
b.states[frag.end].eps.push(frag.start, e);
|
|
969
|
-
return { start: frag.start, end: e };
|
|
1197
|
+
if (attempt >= retries) {
|
|
1198
|
+
const holder = existing === "young" ? null : existing;
|
|
1199
|
+
throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
|
|
970
1200
|
}
|
|
1201
|
+
attempt++;
|
|
1202
|
+
await sleep(delay);
|
|
971
1203
|
}
|
|
972
1204
|
}
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
// src/lessons/regex-linear/nfa.ts
|
|
980
|
-
function wordBoundary(input, pos) {
|
|
981
|
-
const before = pos > 0 && /[A-Za-z0-9_]/.test(input[pos - 1]);
|
|
982
|
-
const after = pos < input.length && /[A-Za-z0-9_]/.test(input[pos]);
|
|
983
|
-
return before !== after;
|
|
984
|
-
}
|
|
985
|
-
function assertHolds(kind, input, pos) {
|
|
986
|
-
switch (kind) {
|
|
987
|
-
case "start":
|
|
988
|
-
return pos === 0;
|
|
989
|
-
case "end":
|
|
990
|
-
return pos === input.length;
|
|
991
|
-
case "wordB":
|
|
992
|
-
return wordBoundary(input, pos);
|
|
993
|
-
case "nonWordB":
|
|
994
|
-
return !wordBoundary(input, pos);
|
|
1205
|
+
async function tryAcquire(lockPath) {
|
|
1206
|
+
try {
|
|
1207
|
+
await mkdir(lockPath, { recursive: false });
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
if (err.code === "EEXIST") return null;
|
|
1210
|
+
throw err;
|
|
995
1211
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
while (stack.length > 0) {
|
|
1002
|
-
if (budget.remaining <= 0) return;
|
|
1003
|
-
const cur = stack.pop();
|
|
1004
|
-
if (set.has(cur)) continue;
|
|
1005
|
-
set.add(cur);
|
|
1006
|
-
budget.remaining -= 1;
|
|
1007
|
-
for (const t of states[cur].eps) if (!set.has(t)) stack.push(t);
|
|
1008
|
-
for (const a of states[cur].asserts) {
|
|
1009
|
-
if (assertHolds(a.kind, input, pos) && !set.has(a.target)) stack.push(a.target);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1212
|
+
const metadataPath = join(lockPath, "holder.json");
|
|
1213
|
+
const metadata = {
|
|
1214
|
+
pid: process.pid,
|
|
1215
|
+
started: Date.now(),
|
|
1216
|
+
hostname: getHostname()
|
|
1012
1217
|
};
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
for (let pos = 0; pos <= input.length; pos += 1) {
|
|
1022
|
-
closure(current, start, input, pos, b);
|
|
1023
|
-
if (current.has(accept)) return true;
|
|
1024
|
-
if (b.remaining <= 0) return false;
|
|
1025
|
-
if (pos === input.length) break;
|
|
1026
|
-
const ch = input[pos];
|
|
1027
|
-
const next = /* @__PURE__ */ new Set();
|
|
1028
|
-
for (const s of current) {
|
|
1029
|
-
b.remaining -= 1;
|
|
1030
|
-
for (const t of states[s].chars) {
|
|
1031
|
-
if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
current = next;
|
|
1035
|
-
}
|
|
1036
|
-
return current.has(accept);
|
|
1218
|
+
await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
1219
|
+
let released = false;
|
|
1220
|
+
const cleanup = () => {
|
|
1221
|
+
if (released) return;
|
|
1222
|
+
released = true;
|
|
1223
|
+
try {
|
|
1224
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
1225
|
+
} catch {
|
|
1037
1226
|
}
|
|
1038
1227
|
};
|
|
1228
|
+
const signalHandler = (signal) => {
|
|
1229
|
+
cleanup();
|
|
1230
|
+
process.kill(process.pid, signal);
|
|
1231
|
+
};
|
|
1232
|
+
process.once("SIGINT", signalHandler);
|
|
1233
|
+
process.once("SIGTERM", signalHandler);
|
|
1234
|
+
process.once("exit", cleanup);
|
|
1235
|
+
return async () => {
|
|
1236
|
+
if (released) return;
|
|
1237
|
+
released = true;
|
|
1238
|
+
process.off("SIGINT", signalHandler);
|
|
1239
|
+
process.off("SIGTERM", signalHandler);
|
|
1240
|
+
process.off("exit", cleanup);
|
|
1241
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
1242
|
+
});
|
|
1243
|
+
};
|
|
1039
1244
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
for (let k = 0; k < min; k += 1) items.push(atom);
|
|
1055
|
-
if (max === Infinity) {
|
|
1056
|
-
items.push({ k: "star", node: atom });
|
|
1057
|
-
} else {
|
|
1058
|
-
for (let k = min; k < max; k += 1) items.push({ k: "opt", node: atom });
|
|
1245
|
+
async function inspectLock(lockPath) {
|
|
1246
|
+
try {
|
|
1247
|
+
const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
|
|
1248
|
+
const parsed = JSON.parse(raw);
|
|
1249
|
+
if (!isLockMetadata(parsed)) return null;
|
|
1250
|
+
return parsed;
|
|
1251
|
+
} catch {
|
|
1252
|
+
try {
|
|
1253
|
+
const info = await stat(lockPath);
|
|
1254
|
+
const ageMs = Date.now() - info.mtimeMs;
|
|
1255
|
+
if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
return null;
|
|
1059
1259
|
}
|
|
1060
|
-
if (items.length === 0) return { k: "empty" };
|
|
1061
|
-
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
1062
1260
|
}
|
|
1063
|
-
function
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
return (x) => !(x >= "0" && x <= "9");
|
|
1069
|
-
case "w":
|
|
1070
|
-
return isWord;
|
|
1071
|
-
case "W":
|
|
1072
|
-
return (x) => !isWord(x);
|
|
1073
|
-
case "s":
|
|
1074
|
-
return (x) => /\s/.test(x);
|
|
1075
|
-
case "S":
|
|
1076
|
-
return (x) => !/\s/.test(x);
|
|
1077
|
-
default:
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1261
|
+
function isStale(meta, staleMs) {
|
|
1262
|
+
if (!meta) return true;
|
|
1263
|
+
const sameHost = !meta.hostname || meta.hostname === getHostname();
|
|
1264
|
+
if (sameHost && !isProcessAlive(meta.pid)) return true;
|
|
1265
|
+
return Date.now() - meta.started > staleMs;
|
|
1080
1266
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1267
|
+
function isProcessAlive(pid) {
|
|
1268
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1269
|
+
try {
|
|
1270
|
+
process.kill(pid, 0);
|
|
1271
|
+
return true;
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
return err.code === "EPERM";
|
|
1087
1274
|
}
|
|
1088
|
-
const hex = src.slice(i, i + 4);
|
|
1089
|
-
return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
|
|
1090
1275
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
|
|
1276
|
+
function describeHolder(meta) {
|
|
1277
|
+
if (!meta) return "unknown (unreadable lock metadata)";
|
|
1278
|
+
const host = meta.hostname ? `${meta.hostname}:` : "";
|
|
1279
|
+
return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
|
|
1097
1280
|
}
|
|
1098
|
-
function
|
|
1099
|
-
if (
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
return { ch: escapeLiteral(e), len: 0 };
|
|
1281
|
+
function isLockMetadata(value) {
|
|
1282
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1283
|
+
const v = value;
|
|
1284
|
+
return typeof v.pid === "number" && typeof v.started === "number";
|
|
1103
1285
|
}
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return "\n";
|
|
1110
|
-
case "r":
|
|
1111
|
-
return "\r";
|
|
1112
|
-
case "f":
|
|
1113
|
-
return "\f";
|
|
1114
|
-
case "v":
|
|
1115
|
-
return "\v";
|
|
1116
|
-
case "0":
|
|
1117
|
-
return "\0";
|
|
1118
|
-
default:
|
|
1119
|
-
return c;
|
|
1120
|
-
}
|
|
1286
|
+
function getHostname() {
|
|
1287
|
+
return hostname();
|
|
1288
|
+
}
|
|
1289
|
+
function sleep(ms) {
|
|
1290
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1121
1291
|
}
|
|
1122
1292
|
|
|
1123
|
-
// src/lessons/
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1293
|
+
// src/lessons/lessons-lock.ts
|
|
1294
|
+
var LESSONS_LOCK_FILENAME = ".lessons.lock";
|
|
1295
|
+
function lessonsLockPath(projectRoot) {
|
|
1296
|
+
return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
|
|
1297
|
+
}
|
|
1298
|
+
async function acquireLessonsLock(projectRoot, opts = {}) {
|
|
1299
|
+
const lockPath = lessonsLockPath(projectRoot);
|
|
1300
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
1301
|
+
return acquireProcessLock(lockPath, { ...opts, label: "lessons lock" });
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/lessons/validate-checks.ts
|
|
1305
|
+
function collectDanglingRefs(graph, findings) {
|
|
1306
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1307
|
+
for (const topicId of lesson.topics) {
|
|
1308
|
+
if (graph.topics[topicId] === void 0) {
|
|
1309
|
+
findings.push({
|
|
1310
|
+
level: "error",
|
|
1311
|
+
code: "DANGLING_TOPIC",
|
|
1312
|
+
message: `Lesson "${lessonId}" references unknown topic "${topicId}".`,
|
|
1313
|
+
lessonId,
|
|
1314
|
+
topicId
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1133
1317
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1318
|
+
for (const triggerId of lesson.triggers) {
|
|
1319
|
+
if (graph.triggers[triggerId] === void 0) {
|
|
1320
|
+
findings.push({
|
|
1321
|
+
level: "error",
|
|
1322
|
+
code: "DANGLING_TRIGGER",
|
|
1323
|
+
message: `Lesson "${lessonId}" references unknown trigger "${triggerId}".`,
|
|
1324
|
+
lessonId,
|
|
1325
|
+
triggerId
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (lesson.supersededBy !== void 0 && graph.lessons[lesson.supersededBy] === void 0) {
|
|
1330
|
+
findings.push({
|
|
1331
|
+
level: "error",
|
|
1332
|
+
code: "DANGLING_SUPERSEDER",
|
|
1333
|
+
message: `Lesson "${lessonId}" supersededBy unknown lesson "${lesson.supersededBy}".`,
|
|
1334
|
+
lessonId
|
|
1335
|
+
});
|
|
1140
1336
|
}
|
|
1141
|
-
if (items.length === 0) return { k: "empty" };
|
|
1142
|
-
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
1143
1337
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1338
|
+
}
|
|
1339
|
+
function collectDuplicateRefs(graph, findings) {
|
|
1340
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1341
|
+
for (const topicId of firstDuplicates(lesson.topics)) {
|
|
1342
|
+
findings.push({
|
|
1343
|
+
level: "error",
|
|
1344
|
+
code: "DUPLICATE_TOPIC_REF",
|
|
1345
|
+
message: `Lesson "${lessonId}" references topic "${topicId}" more than once.`,
|
|
1346
|
+
lessonId,
|
|
1347
|
+
topicId
|
|
1348
|
+
});
|
|
1151
1349
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1350
|
+
for (const triggerId of firstDuplicates(lesson.triggers)) {
|
|
1351
|
+
findings.push({
|
|
1352
|
+
level: "error",
|
|
1353
|
+
code: "DUPLICATE_TRIGGER_REF",
|
|
1354
|
+
message: `Lesson "${lessonId}" references trigger "${triggerId}" more than once.`,
|
|
1355
|
+
lessonId,
|
|
1356
|
+
triggerId
|
|
1357
|
+
});
|
|
1155
1358
|
}
|
|
1156
|
-
return atom;
|
|
1157
1359
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
|
|
1166
|
-
throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
|
|
1167
|
-
}
|
|
1168
|
-
return { min, max };
|
|
1360
|
+
}
|
|
1361
|
+
function firstDuplicates(ids) {
|
|
1362
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1363
|
+
const dup = /* @__PURE__ */ new Set();
|
|
1364
|
+
for (const id of ids) {
|
|
1365
|
+
if (seen.has(id)) dup.add(id);
|
|
1366
|
+
else seen.add(id);
|
|
1169
1367
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1368
|
+
return [...dup];
|
|
1369
|
+
}
|
|
1370
|
+
function collectStatusInvariants(graph, findings) {
|
|
1371
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1372
|
+
if (lesson.status === "superseded" && lesson.supersededBy === void 0) {
|
|
1373
|
+
findings.push({
|
|
1374
|
+
level: "error",
|
|
1375
|
+
code: "SUPERSEDED_WITHOUT_TARGET",
|
|
1376
|
+
message: `Lesson "${lessonId}" has status "superseded" but no supersededBy target.`,
|
|
1377
|
+
lessonId
|
|
1378
|
+
});
|
|
1178
1379
|
}
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1181
|
-
|
|
1380
|
+
if (lesson.status === "active" && lesson.supersededBy !== void 0) {
|
|
1381
|
+
findings.push({
|
|
1382
|
+
level: "error",
|
|
1383
|
+
code: "ACTIVE_WITH_SUPERSEDER",
|
|
1384
|
+
message: `Lesson "${lessonId}" has status "active" but declares supersededBy.`,
|
|
1385
|
+
lessonId
|
|
1386
|
+
});
|
|
1182
1387
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function collectLifecycleInvariants(graph, findings) {
|
|
1391
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1392
|
+
if (lesson.supersededBy === void 0) continue;
|
|
1393
|
+
if (lesson.supersededBy === lessonId) {
|
|
1394
|
+
findings.push({
|
|
1395
|
+
level: "error",
|
|
1396
|
+
code: "SELF_SUPERSEDED",
|
|
1397
|
+
message: `Lesson "${lessonId}" is superseded by itself.`,
|
|
1398
|
+
lessonId
|
|
1399
|
+
});
|
|
1400
|
+
continue;
|
|
1186
1401
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1402
|
+
const target = graph.lessons[lesson.supersededBy];
|
|
1403
|
+
if (target !== void 0 && target.status !== "active") {
|
|
1404
|
+
findings.push({
|
|
1405
|
+
level: "error",
|
|
1406
|
+
code: "INACTIVE_SUPERSEDER",
|
|
1407
|
+
message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target.status} \u2014 the chain dead-ends with no live replacement.`,
|
|
1408
|
+
lessonId
|
|
1409
|
+
});
|
|
1189
1410
|
}
|
|
1190
|
-
i += 1;
|
|
1191
|
-
return { k: "char", ch: c };
|
|
1192
1411
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1412
|
+
collectSupersedeCycles(graph, findings);
|
|
1413
|
+
}
|
|
1414
|
+
function collectSupersedeCycles(graph, findings) {
|
|
1415
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1416
|
+
for (const startId of Object.keys(graph.lessons)) {
|
|
1417
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1418
|
+
let cur = startId;
|
|
1419
|
+
while (cur !== void 0) {
|
|
1420
|
+
if (seen.has(cur)) {
|
|
1421
|
+
if (cur !== startId || reported.has(cur)) break;
|
|
1422
|
+
reported.add(cur);
|
|
1423
|
+
findings.push({
|
|
1424
|
+
level: "error",
|
|
1425
|
+
code: "SUPERSEDE_CYCLE",
|
|
1426
|
+
message: `Lesson "${startId}" is part of a supersededBy cycle.`,
|
|
1427
|
+
lessonId: startId
|
|
1428
|
+
});
|
|
1429
|
+
break;
|
|
1207
1430
|
}
|
|
1431
|
+
seen.add(cur);
|
|
1432
|
+
const next = graph.lessons[cur]?.supersededBy;
|
|
1433
|
+
if (next === cur) break;
|
|
1434
|
+
cur = next;
|
|
1208
1435
|
}
|
|
1209
|
-
const inner = parseAlt();
|
|
1210
|
-
if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
|
|
1211
|
-
i += 1;
|
|
1212
|
-
return inner;
|
|
1213
1436
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (cls !== null) return { k: "class", test: cls };
|
|
1225
|
-
if (c === "x" || c === "u" || c === "c") {
|
|
1226
|
-
const { ch, len } = c === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c);
|
|
1227
|
-
i += len;
|
|
1228
|
-
return { k: "char", ch };
|
|
1437
|
+
}
|
|
1438
|
+
function collectReachability(graph, findings) {
|
|
1439
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1440
|
+
if (lesson.status === "active" && lesson.triggers.length === 0) {
|
|
1441
|
+
findings.push({
|
|
1442
|
+
level: "warning",
|
|
1443
|
+
code: "UNREACHABLE_LESSON",
|
|
1444
|
+
message: `Active lesson "${lessonId}" has no triggers and can never be recalled.`,
|
|
1445
|
+
lessonId
|
|
1446
|
+
});
|
|
1229
1447
|
}
|
|
1230
|
-
return { k: "char", ch: escapeLiteral(c) };
|
|
1231
1448
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
}
|
|
1240
|
-
if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
|
|
1241
|
-
i += 1;
|
|
1242
|
-
const base = (c) => tests.some((t) => t(c));
|
|
1243
|
-
return { k: "class", test: negate ? (c) => !base(c) : base };
|
|
1449
|
+
}
|
|
1450
|
+
function collectOrphans(graph, findings) {
|
|
1451
|
+
const referencedTopics = /* @__PURE__ */ new Set();
|
|
1452
|
+
const referencedTriggers = /* @__PURE__ */ new Set();
|
|
1453
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1454
|
+
for (const t of lesson.topics) referencedTopics.add(t);
|
|
1455
|
+
for (const t of lesson.triggers) referencedTriggers.add(t);
|
|
1244
1456
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
i += r.len;
|
|
1254
|
-
lo = r.ch;
|
|
1255
|
-
} else {
|
|
1256
|
-
lo = eat();
|
|
1257
|
-
}
|
|
1258
|
-
if (peek() === "-" && src[i + 1] !== void 0 && src[i + 1] !== "]") {
|
|
1259
|
-
i += 1;
|
|
1260
|
-
let hi;
|
|
1261
|
-
if (peek() === "\\") {
|
|
1262
|
-
i += 1;
|
|
1263
|
-
const e2 = eat();
|
|
1264
|
-
const r = classEscapeChar(src, i, e2);
|
|
1265
|
-
i += r.len;
|
|
1266
|
-
hi = r.ch;
|
|
1267
|
-
} else {
|
|
1268
|
-
hi = eat();
|
|
1269
|
-
}
|
|
1270
|
-
const a = lo.codePointAt(0);
|
|
1271
|
-
const b = hi.codePointAt(0);
|
|
1272
|
-
return (c) => {
|
|
1273
|
-
const p = c.codePointAt(0);
|
|
1274
|
-
return p >= a && p <= b;
|
|
1275
|
-
};
|
|
1457
|
+
for (const topicId of Object.keys(graph.topics)) {
|
|
1458
|
+
if (!referencedTopics.has(topicId)) {
|
|
1459
|
+
findings.push({
|
|
1460
|
+
level: "warning",
|
|
1461
|
+
code: "ORPHAN_TOPIC",
|
|
1462
|
+
message: `Topic "${topicId}" is not referenced by any lesson.`,
|
|
1463
|
+
topicId
|
|
1464
|
+
});
|
|
1276
1465
|
}
|
|
1277
|
-
return (c) => c === lo;
|
|
1278
1466
|
}
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
|
|
1289
|
-
let matcher;
|
|
1290
|
-
try {
|
|
1291
|
-
matcher = buildMatcher(parseRegex(pattern));
|
|
1292
|
-
} catch {
|
|
1293
|
-
matcher = null;
|
|
1467
|
+
for (const triggerId of Object.keys(graph.triggers)) {
|
|
1468
|
+
if (!referencedTriggers.has(triggerId)) {
|
|
1469
|
+
findings.push({
|
|
1470
|
+
level: "warning",
|
|
1471
|
+
code: "ORPHAN_TRIGGER",
|
|
1472
|
+
message: `Trigger "${triggerId}" is not referenced by any lesson.`,
|
|
1473
|
+
triggerId
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1294
1476
|
}
|
|
1295
|
-
cache.set(pattern, matcher);
|
|
1296
|
-
return matcher;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// src/lessons/regex-safety.ts
|
|
1300
|
-
var MAX_PATTERN_LENGTH = 1e3;
|
|
1301
|
-
function isSafeRegexPattern(pattern) {
|
|
1302
|
-
if (pattern.length > MAX_PATTERN_LENGTH) return false;
|
|
1303
|
-
return compileLinearMatcher(pattern) !== null;
|
|
1304
|
-
}
|
|
1305
|
-
function getCommandMatcher(pattern) {
|
|
1306
|
-
if (pattern.length > MAX_PATTERN_LENGTH) return null;
|
|
1307
|
-
return compileLinearMatcher(pattern);
|
|
1308
1477
|
}
|
|
1309
1478
|
|
|
1310
1479
|
// src/lessons/validate-quality.ts
|
|
@@ -1410,6 +1579,11 @@ function collectFanout(graph, findings) {
|
|
|
1410
1579
|
});
|
|
1411
1580
|
}
|
|
1412
1581
|
}
|
|
1582
|
+
function normalizeRule2(rule) {
|
|
1583
|
+
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/lessons/validate-keywords.ts
|
|
1413
1587
|
function collectLowSignalKeywords(graph, findings) {
|
|
1414
1588
|
const activeTriggerIds2 = /* @__PURE__ */ new Set();
|
|
1415
1589
|
for (const lesson of Object.values(graph.lessons)) {
|
|
@@ -1423,7 +1597,7 @@ function collectLowSignalKeywords(graph, findings) {
|
|
|
1423
1597
|
findings.push({
|
|
1424
1598
|
level: "warning",
|
|
1425
1599
|
code: "LOW_SIGNAL_KEYWORD",
|
|
1426
|
-
message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a
|
|
1600
|
+
message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a contiguous token-run in --keyword or the file/command, so it rarely fires \u2014 use a short distinctive phrase.`,
|
|
1427
1601
|
triggerId
|
|
1428
1602
|
});
|
|
1429
1603
|
}
|
|
@@ -1448,9 +1622,6 @@ function collectStopwordKeywords(graph, findings) {
|
|
|
1448
1622
|
});
|
|
1449
1623
|
}
|
|
1450
1624
|
}
|
|
1451
|
-
function normalizeRule2(rule) {
|
|
1452
|
-
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1453
|
-
}
|
|
1454
1625
|
|
|
1455
1626
|
// src/lessons/validate.ts
|
|
1456
1627
|
function validateLessonsGraph(graph, options = {}) {
|
|
@@ -1478,6 +1649,8 @@ function validateLessonsGraph(graph, options = {}) {
|
|
|
1478
1649
|
collectLowSignalKeywords(graph, findings);
|
|
1479
1650
|
collectStopwordKeywords(graph, findings);
|
|
1480
1651
|
collectRunnerAnchoredPatterns(graph, findings);
|
|
1652
|
+
collectBroadCommandPatterns(graph, findings);
|
|
1653
|
+
collectBroadFileGlobs(graph, findings);
|
|
1481
1654
|
if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
|
|
1482
1655
|
const ok = findings.every((f) => f.level !== "error");
|
|
1483
1656
|
return { ok, findings };
|
|
@@ -1509,64 +1682,19 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
|
|
|
1509
1682
|
`mutateLessonsGraph: refusing to write \u2014 this change introduces ${errors}. (Pre-existing graph issues are not blocking; run \`agentsmesh lessons validate\` to review and \`lessons untrigger\`/\`prune\` to repair them.)`
|
|
1510
1683
|
);
|
|
1511
1684
|
}
|
|
1512
|
-
graph.version = CURRENT_GRAPH_VERSION;
|
|
1513
|
-
saveLessonsGraph(projectRoot, graph);
|
|
1514
|
-
return result;
|
|
1515
|
-
} finally {
|
|
1516
|
-
await release();
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
|
|
1520
|
-
await maybeAutoMigrateLessons(projectRoot);
|
|
1521
|
-
return mutateLessonsGraphLocked(projectRoot, mutator, options);
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// src/lessons/trigger-effectiveness.ts
|
|
1525
|
-
function ineffectiveTriggers(graph, triggerIds) {
|
|
1526
|
-
const out = [];
|
|
1527
|
-
for (const id of triggerIds) {
|
|
1528
|
-
const trigger = graph.triggers[id];
|
|
1529
|
-
if (trigger === void 0) continue;
|
|
1530
|
-
const reason = ineffectiveReason(trigger.kind, trigger.pattern);
|
|
1531
|
-
if (reason !== null) out.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
|
|
1532
|
-
}
|
|
1533
|
-
return out;
|
|
1534
|
-
}
|
|
1535
|
-
function ineffectiveReason(kind, pattern) {
|
|
1536
|
-
if (kind === "keyword") {
|
|
1537
|
-
if (tokenize(pattern).length === 0) {
|
|
1538
|
-
return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
|
|
1539
|
-
}
|
|
1540
|
-
if (keywordNeedleLosesTokens(pattern)) {
|
|
1541
|
-
return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
|
|
1542
|
-
}
|
|
1543
|
-
return null;
|
|
1544
|
-
}
|
|
1545
|
-
if (kind === "command_pattern") {
|
|
1546
|
-
let valid = true;
|
|
1547
|
-
try {
|
|
1548
|
-
new RegExp(pattern);
|
|
1549
|
-
} catch {
|
|
1550
|
-
valid = false;
|
|
1551
|
-
}
|
|
1552
|
-
if (!valid) {
|
|
1553
|
-
return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
|
|
1554
|
-
}
|
|
1555
|
-
if (!isSafeRegexPattern(pattern)) {
|
|
1556
|
-
return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
|
|
1557
|
-
}
|
|
1558
|
-
return null;
|
|
1685
|
+
graph.version = CURRENT_GRAPH_VERSION;
|
|
1686
|
+
saveLessonsGraph(projectRoot, graph);
|
|
1687
|
+
return result;
|
|
1688
|
+
} finally {
|
|
1689
|
+
await release();
|
|
1559
1690
|
}
|
|
1560
|
-
return null;
|
|
1561
1691
|
}
|
|
1562
|
-
function
|
|
1563
|
-
|
|
1692
|
+
async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
|
|
1693
|
+
await maybeAutoMigrateLessons(projectRoot);
|
|
1694
|
+
return mutateLessonsGraphLocked(projectRoot, mutator, options);
|
|
1564
1695
|
}
|
|
1565
1696
|
|
|
1566
1697
|
// src/lessons/add.ts
|
|
1567
|
-
function countInputTriggers(triggers) {
|
|
1568
|
-
return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
|
|
1569
|
-
}
|
|
1570
1698
|
async function addLesson(projectRoot, input, options = {}) {
|
|
1571
1699
|
return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
|
|
1572
1700
|
retries: options.retries
|
|
@@ -1574,10 +1702,7 @@ async function addLesson(projectRoot, input, options = {}) {
|
|
|
1574
1702
|
}
|
|
1575
1703
|
function addLessonInto(graph, input, options) {
|
|
1576
1704
|
const ruleKey = normalizeRule(input.rule);
|
|
1577
|
-
const trimmedRule = input.rule
|
|
1578
|
-
if (trimmedRule.length > MAX_RULE_LENGTH) {
|
|
1579
|
-
throw new RuleTooLongError(trimmedRule.length, MAX_RULE_LENGTH);
|
|
1580
|
-
}
|
|
1705
|
+
const trimmedRule = assertRuleShape(input.rule);
|
|
1581
1706
|
const existingId = findExistingLessonByRule(graph, ruleKey);
|
|
1582
1707
|
const isNewTopic = graph.topics[input.topic] === void 0;
|
|
1583
1708
|
if (isNewTopic) {
|
|
@@ -1587,29 +1712,23 @@ function addLessonInto(graph, input, options) {
|
|
|
1587
1712
|
}
|
|
1588
1713
|
graph.topics[input.topic] = { summary: options.topicSummary };
|
|
1589
1714
|
}
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
|
|
1593
|
-
if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
|
|
1594
|
-
throw new NoTriggerError();
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1715
|
+
const existing = existingId !== null ? graph.lessons[existingId] : void 0;
|
|
1716
|
+
assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
|
|
1597
1717
|
const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
|
|
1598
|
-
if (!
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1718
|
+
if (!skipsTriggerGates(input, options)) {
|
|
1719
|
+
assertRecallable(
|
|
1720
|
+
graph,
|
|
1721
|
+
existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
|
|
1722
|
+
);
|
|
1604
1723
|
}
|
|
1605
1724
|
if (existingId !== null) {
|
|
1606
|
-
const
|
|
1725
|
+
const existing2 = graph.lessons[existingId];
|
|
1607
1726
|
graph.lessons[existingId] = {
|
|
1608
|
-
...
|
|
1609
|
-
topics: union(
|
|
1610
|
-
triggers: union(
|
|
1611
|
-
evidence: union(
|
|
1612
|
-
...
|
|
1727
|
+
...existing2,
|
|
1728
|
+
topics: union(existing2.topics, [input.topic]),
|
|
1729
|
+
triggers: union(existing2.triggers, triggerIds),
|
|
1730
|
+
evidence: union(existing2.evidence, input.evidence ?? []),
|
|
1731
|
+
...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
|
|
1613
1732
|
// Re-capturing a rule with --scope always promotes it to always-on.
|
|
1614
1733
|
...input.scope === "always" ? { scope: "always" } : {}
|
|
1615
1734
|
};
|
|
@@ -1702,7 +1821,7 @@ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
|
1702
1821
|
|
|
1703
1822
|
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
1704
1823
|
|
|
1705
|
-
**Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command
|
|
1824
|
+
**Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command> --session auto\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always --session auto\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
|
|
1706
1825
|
|
|
1707
1826
|
**Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
|
|
1708
1827
|
|
|
@@ -1806,26 +1925,6 @@ async function maybeAutoMigrateLessons(projectRoot) {
|
|
|
1806
1925
|
throw err;
|
|
1807
1926
|
}
|
|
1808
1927
|
}
|
|
1809
|
-
function normalizeRecallFile(file, projectRoot) {
|
|
1810
|
-
const forward = file.replaceAll("\\", "/");
|
|
1811
|
-
const direct = relativize(projectRoot, forward);
|
|
1812
|
-
if (!direct.startsWith("../")) return direct;
|
|
1813
|
-
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
1814
|
-
return viaReal.startsWith("../") ? direct : viaReal;
|
|
1815
|
-
}
|
|
1816
|
-
function relativize(root, forward) {
|
|
1817
|
-
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
1818
|
-
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
1819
|
-
}
|
|
1820
|
-
function safeRealpath(path) {
|
|
1821
|
-
try {
|
|
1822
|
-
return realpathSync(path);
|
|
1823
|
-
} catch {
|
|
1824
|
-
const parent = dirname(path);
|
|
1825
|
-
if (parent === path) return path;
|
|
1826
|
-
return resolve(safeRealpath(parent), basename(path));
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
1928
|
|
|
1830
1929
|
// src/lessons/keyword-match.ts
|
|
1831
1930
|
function deriveHaystackTokens(query) {
|
|
@@ -1834,14 +1933,16 @@ function deriveHaystackTokens(query) {
|
|
|
1834
1933
|
if (query.command !== void 0) parts.push(query.command);
|
|
1835
1934
|
if (parts.length === 0) return [];
|
|
1836
1935
|
const out = [];
|
|
1837
|
-
for (const raw of parts.join(" ")
|
|
1838
|
-
if (raw.length === 0) continue;
|
|
1936
|
+
for (const raw of splitTokens(parts.join(" "))) {
|
|
1839
1937
|
out.push(raw.toLowerCase());
|
|
1840
1938
|
const sub = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(" ").filter((t) => t.length > 0);
|
|
1841
1939
|
if (sub.length > 1) out.push(...sub);
|
|
1842
1940
|
}
|
|
1843
1941
|
return out;
|
|
1844
1942
|
}
|
|
1943
|
+
function splitTokens(text) {
|
|
1944
|
+
return text.split(/[^A-Za-z0-9]+/).filter((t) => t.length > 0);
|
|
1945
|
+
}
|
|
1845
1946
|
function containsRun(needle, hay) {
|
|
1846
1947
|
if (needle.length === 0) return false;
|
|
1847
1948
|
for (let i = 0; i + needle.length <= hay.length; i += 1) {
|
|
@@ -1857,10 +1958,11 @@ function containsRun(needle, hay) {
|
|
|
1857
1958
|
return false;
|
|
1858
1959
|
}
|
|
1859
1960
|
function keywordMatches(pattern, query) {
|
|
1860
|
-
|
|
1961
|
+
const needle = tokenize(pattern);
|
|
1962
|
+
if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
|
|
1861
1963
|
return true;
|
|
1862
1964
|
}
|
|
1863
|
-
return containsRun(
|
|
1965
|
+
return containsRun(needle, deriveHaystackTokens(query));
|
|
1864
1966
|
}
|
|
1865
1967
|
|
|
1866
1968
|
// src/lessons/query.ts
|
|
@@ -1911,6 +2013,148 @@ function triggerMatches(trigger, query, budget) {
|
|
|
1911
2013
|
return keywordMatches(trigger.pattern, query);
|
|
1912
2014
|
}
|
|
1913
2015
|
}
|
|
2016
|
+
function appendJsonl(path, record, opts) {
|
|
2017
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2018
|
+
appendFileSync(path, `${JSON.stringify(record)}
|
|
2019
|
+
`, "utf8");
|
|
2020
|
+
if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
|
|
2021
|
+
}
|
|
2022
|
+
function capJsonl(path, maxRecords) {
|
|
2023
|
+
if (!existsSync(path)) return;
|
|
2024
|
+
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
2025
|
+
if (lines.length <= maxRecords) return;
|
|
2026
|
+
const kept = lines.slice(lines.length - maxRecords);
|
|
2027
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2028
|
+
writeFileSync(tmp, `${kept.join("\n")}
|
|
2029
|
+
`, "utf8");
|
|
2030
|
+
renameSync(tmp, path);
|
|
2031
|
+
}
|
|
2032
|
+
function readJsonl(path) {
|
|
2033
|
+
if (!existsSync(path)) return [];
|
|
2034
|
+
const out = [];
|
|
2035
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
2036
|
+
if (line.trim().length === 0) continue;
|
|
2037
|
+
try {
|
|
2038
|
+
out.push(JSON.parse(line));
|
|
2039
|
+
} catch {
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return out;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/lessons/telemetry.ts
|
|
2046
|
+
var MAX_RECALL_LOG_RECORDS = 5e3;
|
|
2047
|
+
var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
2048
|
+
var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
|
|
2049
|
+
var SESSION_ENV = "AGENTSMESH_SESSION_ID";
|
|
2050
|
+
function sessionId(env = process.env) {
|
|
2051
|
+
const raw = env[SESSION_ENV];
|
|
2052
|
+
return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
|
|
2053
|
+
}
|
|
2054
|
+
function recallLogPath(projectRoot) {
|
|
2055
|
+
return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
|
|
2056
|
+
}
|
|
2057
|
+
function isTelemetryEnabled(env = process.env) {
|
|
2058
|
+
return env[TELEMETRY_ENV] === "1";
|
|
2059
|
+
}
|
|
2060
|
+
function appendRecallRecord(projectRoot, record, env = process.env) {
|
|
2061
|
+
if (!isTelemetryEnabled(env)) return;
|
|
2062
|
+
appendJsonl(recallLogPath(projectRoot), record, {
|
|
2063
|
+
maxRecords: MAX_RECALL_LOG_RECORDS,
|
|
2064
|
+
trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/lessons/cmd-fastpath.ts
|
|
2069
|
+
var FASTPATH_DIR = "agentsmesh-lessons-cmdidx";
|
|
2070
|
+
function shortHash(value) {
|
|
2071
|
+
let h = 5381;
|
|
2072
|
+
for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
|
|
2073
|
+
return h.toString(36);
|
|
2074
|
+
}
|
|
2075
|
+
function commandFastpathCachePath(projectRoot) {
|
|
2076
|
+
return join(tmpdir(), FASTPATH_DIR, `${shortHash(resolve(projectRoot))}.json`);
|
|
2077
|
+
}
|
|
2078
|
+
function currentGraphStamp(projectRoot) {
|
|
2079
|
+
try {
|
|
2080
|
+
const s = statSync(graphFilePath(projectRoot));
|
|
2081
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
2082
|
+
} catch {
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function isStringArray(v) {
|
|
2087
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
2088
|
+
}
|
|
2089
|
+
function readCache(path) {
|
|
2090
|
+
try {
|
|
2091
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2092
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2093
|
+
const c = parsed;
|
|
2094
|
+
const stamp = c.stamp;
|
|
2095
|
+
if (typeof stamp?.mtimeMs !== "number" || typeof stamp.size !== "number" || !isStringArray(c.commandPatterns) || !isStringArray(c.keywordPatterns)) {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
return {
|
|
2099
|
+
stamp: { mtimeMs: stamp.mtimeMs, size: stamp.size },
|
|
2100
|
+
commandPatterns: c.commandPatterns,
|
|
2101
|
+
keywordPatterns: c.keywordPatterns
|
|
2102
|
+
};
|
|
2103
|
+
} catch {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function refreshCommandFastpath(projectRoot, graph, preReadStamp) {
|
|
2108
|
+
try {
|
|
2109
|
+
if (preReadStamp === null) return;
|
|
2110
|
+
const stamp = currentGraphStamp(projectRoot);
|
|
2111
|
+
if (stamp === null) return;
|
|
2112
|
+
if (stamp.mtimeMs !== preReadStamp.mtimeMs || stamp.size !== preReadStamp.size) return;
|
|
2113
|
+
const path = commandFastpathCachePath(projectRoot);
|
|
2114
|
+
const existing = readCache(path);
|
|
2115
|
+
if (existing !== null && existing.stamp.mtimeMs === stamp.mtimeMs && existing.stamp.size === stamp.size) {
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
2119
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
2120
|
+
if (lesson.status !== "active" || lesson.scope === "always") continue;
|
|
2121
|
+
for (const t of lesson.triggers) reachable.add(t);
|
|
2122
|
+
}
|
|
2123
|
+
const commandPatterns = [];
|
|
2124
|
+
const keywordPatterns = [];
|
|
2125
|
+
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
2126
|
+
if (!reachable.has(id)) continue;
|
|
2127
|
+
if (trigger.kind === "command_pattern") commandPatterns.push(trigger.pattern);
|
|
2128
|
+
else if (trigger.kind === "keyword") keywordPatterns.push(trigger.pattern);
|
|
2129
|
+
}
|
|
2130
|
+
const cache2 = { stamp, commandPatterns, keywordPatterns };
|
|
2131
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2132
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2133
|
+
writeFileSync(tmp, JSON.stringify(cache2), "utf8");
|
|
2134
|
+
renameSync(tmp, path);
|
|
2135
|
+
} catch {
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
function normalizeRecallFile(file, projectRoot) {
|
|
2139
|
+
const forward = file.replaceAll("\\", "/");
|
|
2140
|
+
const direct = relativize(projectRoot, forward);
|
|
2141
|
+
if (!direct.startsWith("../")) return direct;
|
|
2142
|
+
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
2143
|
+
return viaReal.startsWith("../") ? direct : viaReal;
|
|
2144
|
+
}
|
|
2145
|
+
function relativize(root, forward) {
|
|
2146
|
+
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
2147
|
+
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
2148
|
+
}
|
|
2149
|
+
function safeRealpath(path) {
|
|
2150
|
+
try {
|
|
2151
|
+
return realpathSync(path);
|
|
2152
|
+
} catch {
|
|
2153
|
+
const parent = dirname(path);
|
|
2154
|
+
if (parent === path) return path;
|
|
2155
|
+
return resolve(safeRealpath(parent), basename(path));
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
1914
2158
|
|
|
1915
2159
|
// src/lessons/ranking-signals.ts
|
|
1916
2160
|
function buildFanout(graph) {
|
|
@@ -1921,6 +2165,15 @@ function buildFanout(graph) {
|
|
|
1921
2165
|
}
|
|
1922
2166
|
return fanout;
|
|
1923
2167
|
}
|
|
2168
|
+
var KEYWORD_NARROWNESS = 0.4;
|
|
2169
|
+
function buildNarrowness(graph) {
|
|
2170
|
+
const narrowness = /* @__PURE__ */ new Map();
|
|
2171
|
+
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
2172
|
+
if (trigger.kind === "file_glob") narrowness.set(id, globNarrowness(trigger.pattern));
|
|
2173
|
+
else narrowness.set(id, trigger.kind === "keyword" ? KEYWORD_NARROWNESS : 1);
|
|
2174
|
+
}
|
|
2175
|
+
return narrowness;
|
|
2176
|
+
}
|
|
1924
2177
|
function buildTopicCoherence(matches) {
|
|
1925
2178
|
const topicCount = /* @__PURE__ */ new Map();
|
|
1926
2179
|
for (const { lesson } of matches) {
|
|
@@ -1939,7 +2192,7 @@ function buildTopicCoherence(matches) {
|
|
|
1939
2192
|
var DEFAULT_RECALL_LIMIT = 10;
|
|
1940
2193
|
var DEFAULT_RECALL_MAX_TOKENS = 400;
|
|
1941
2194
|
var RRF_K = 60;
|
|
1942
|
-
var SPECIFICITY_WEIGHT =
|
|
2195
|
+
var SPECIFICITY_WEIGHT = 5;
|
|
1943
2196
|
var TOPIC_COHERENCE_WEIGHT = 2;
|
|
1944
2197
|
var BM25_WEIGHT = 1;
|
|
1945
2198
|
var EFFECTIVENESS_WEIGHT = 1;
|
|
@@ -1966,12 +2219,15 @@ function rankLessons(graph, query, matches, options = {}) {
|
|
|
1966
2219
|
const terms = queryTerms(query);
|
|
1967
2220
|
const corpus = buildCorpus(graph);
|
|
1968
2221
|
const fanout = buildFanout(graph);
|
|
2222
|
+
const narrowness = buildNarrowness(graph);
|
|
1969
2223
|
const coherence = buildTopicCoherence(matches);
|
|
1970
2224
|
const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
|
|
1971
2225
|
const scored = matches.map(({ id, lesson }) => {
|
|
1972
2226
|
const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
|
|
1973
2227
|
let specificity = 0;
|
|
1974
|
-
for (const t of hitTriggers)
|
|
2228
|
+
for (const t of hitTriggers) {
|
|
2229
|
+
specificity = Math.max(specificity, (narrowness.get(t) ?? 1) / fanout.get(t));
|
|
2230
|
+
}
|
|
1975
2231
|
return {
|
|
1976
2232
|
id,
|
|
1977
2233
|
lesson,
|
|
@@ -2054,57 +2310,6 @@ function loadRecallConfig(projectRoot) {
|
|
|
2054
2310
|
return fallback;
|
|
2055
2311
|
}
|
|
2056
2312
|
}
|
|
2057
|
-
function appendJsonl(path, record, opts) {
|
|
2058
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
2059
|
-
appendFileSync(path, `${JSON.stringify(record)}
|
|
2060
|
-
`, "utf8");
|
|
2061
|
-
if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
|
|
2062
|
-
}
|
|
2063
|
-
function capJsonl(path, maxRecords) {
|
|
2064
|
-
if (!existsSync(path)) return;
|
|
2065
|
-
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
2066
|
-
if (lines.length <= maxRecords) return;
|
|
2067
|
-
const kept = lines.slice(lines.length - maxRecords);
|
|
2068
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
2069
|
-
writeFileSync(tmp, `${kept.join("\n")}
|
|
2070
|
-
`, "utf8");
|
|
2071
|
-
renameSync(tmp, path);
|
|
2072
|
-
}
|
|
2073
|
-
function readJsonl(path) {
|
|
2074
|
-
if (!existsSync(path)) return [];
|
|
2075
|
-
const out = [];
|
|
2076
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
2077
|
-
if (line.trim().length === 0) continue;
|
|
2078
|
-
try {
|
|
2079
|
-
out.push(JSON.parse(line));
|
|
2080
|
-
} catch {
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
return out;
|
|
2084
|
-
}
|
|
2085
|
-
var MAX_RECALL_LOG_RECORDS = 5e3;
|
|
2086
|
-
var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
2087
|
-
var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
|
|
2088
|
-
var SESSION_ENV = "AGENTSMESH_SESSION_ID";
|
|
2089
|
-
function sessionId(env = process.env) {
|
|
2090
|
-
const raw = env[SESSION_ENV];
|
|
2091
|
-
return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
|
|
2092
|
-
}
|
|
2093
|
-
function recallLogPath(projectRoot) {
|
|
2094
|
-
return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
|
|
2095
|
-
}
|
|
2096
|
-
function isTelemetryEnabled(env = process.env) {
|
|
2097
|
-
return env[TELEMETRY_ENV] === "1";
|
|
2098
|
-
}
|
|
2099
|
-
function appendRecallRecord(projectRoot, record, env = process.env) {
|
|
2100
|
-
if (!isTelemetryEnabled(env)) return;
|
|
2101
|
-
appendJsonl(recallLogPath(projectRoot), record, {
|
|
2102
|
-
maxRecords: MAX_RECALL_LOG_RECORDS,
|
|
2103
|
-
trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
// src/lessons/outcome-log.ts
|
|
2108
2313
|
function outcomeLogPath(projectRoot) {
|
|
2109
2314
|
return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
|
|
2110
2315
|
}
|
|
@@ -2141,48 +2346,116 @@ function loadEffectiveness(projectRoot) {
|
|
|
2141
2346
|
return map;
|
|
2142
2347
|
}
|
|
2143
2348
|
var SEEN_DIR = "agentsmesh-lessons-seen";
|
|
2144
|
-
function
|
|
2145
|
-
if (options.disabled === true) return null;
|
|
2146
|
-
const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
|
|
2147
|
-
if (id === void 0) return null;
|
|
2148
|
-
const path = seenPath(id, options.projectRoot);
|
|
2149
|
-
return { sessionId: id, seen: loadSeen(path), path };
|
|
2150
|
-
}
|
|
2151
|
-
function shortHash(value) {
|
|
2349
|
+
function shortHash2(value) {
|
|
2152
2350
|
let h = 5381;
|
|
2153
2351
|
for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
|
|
2154
2352
|
return h.toString(36);
|
|
2155
2353
|
}
|
|
2156
|
-
function
|
|
2354
|
+
function seenStorePath(id, projectRoot) {
|
|
2157
2355
|
const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 200);
|
|
2158
|
-
const scoped = projectRoot === void 0 ? safe : `${safe}__${
|
|
2356
|
+
const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash2(resolve(projectRoot))}`;
|
|
2159
2357
|
return join(tmpdir(), SEEN_DIR, `${scoped}.json`);
|
|
2160
2358
|
}
|
|
2161
|
-
function
|
|
2162
|
-
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
2359
|
+
function readSeenStore(path) {
|
|
2360
|
+
if (!existsSync(path)) return { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2163
2361
|
try {
|
|
2164
2362
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
2363
|
+
if (Array.isArray(parsed)) {
|
|
2364
|
+
const ids = new Set(parsed.filter((x) => typeof x === "string"));
|
|
2365
|
+
return { ids, stamps: null };
|
|
2366
|
+
}
|
|
2367
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2368
|
+
const seen = parsed.seen;
|
|
2369
|
+
if (typeof seen === "object" && seen !== null) {
|
|
2370
|
+
const stamps = /* @__PURE__ */ new Map();
|
|
2371
|
+
for (const [id, ms] of Object.entries(seen)) {
|
|
2372
|
+
if (typeof ms === "number") stamps.set(id, ms);
|
|
2373
|
+
}
|
|
2374
|
+
const lastAt = parsed.lastAt;
|
|
2375
|
+
return {
|
|
2376
|
+
ids: new Set(stamps.keys()),
|
|
2377
|
+
stamps,
|
|
2378
|
+
...typeof lastAt === "number" ? { lastAt } : {}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2383
|
+
} catch {
|
|
2384
|
+
return { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
function writeSeenStore(path, data, lastAt) {
|
|
2388
|
+
try {
|
|
2389
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2390
|
+
const body = data instanceof Map ? JSON.stringify({ v: 2, lastAt: lastAt ?? Date.now(), seen: Object.fromEntries(data) }) : JSON.stringify(data);
|
|
2391
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2392
|
+
writeFileSync(tmp, body, "utf8");
|
|
2393
|
+
renameSync(tmp, path);
|
|
2167
2394
|
} catch {
|
|
2168
|
-
return /* @__PURE__ */ new Set();
|
|
2169
2395
|
}
|
|
2170
2396
|
}
|
|
2397
|
+
var AUTO_SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
2398
|
+
var FUTURE_TOLERANCE_MS = 6e4;
|
|
2399
|
+
function stampAgeMs(stamp, now = Date.now()) {
|
|
2400
|
+
if (stamp - now > FUTURE_TOLERANCE_MS) return Number.POSITIVE_INFINITY;
|
|
2401
|
+
return Math.max(0, now - stamp);
|
|
2402
|
+
}
|
|
2403
|
+
function isIdleSession(stamps, lastAt) {
|
|
2404
|
+
if (stamps === null || stamps.size === 0) return false;
|
|
2405
|
+
let newest = lastAt ?? 0;
|
|
2406
|
+
for (const ms of stamps.values()) if (ms > newest) newest = ms;
|
|
2407
|
+
return stampAgeMs(newest) > AUTO_SESSION_IDLE_MS;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// src/lessons/seen-cache.ts
|
|
2411
|
+
function openSessionDedup(options = {}) {
|
|
2412
|
+
if (options.disabled === true) return null;
|
|
2413
|
+
const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
|
|
2414
|
+
if (id === void 0) return null;
|
|
2415
|
+
const path = seenStorePath(id, options.projectRoot);
|
|
2416
|
+
const store = readSeenStore(path);
|
|
2417
|
+
const stale = options.ttlMs !== void 0 && isIdleSession(store.stamps, store.lastAt);
|
|
2418
|
+
const stamps = stale ? null : store.stamps;
|
|
2419
|
+
return {
|
|
2420
|
+
sessionId: id,
|
|
2421
|
+
seen: stale ? /* @__PURE__ */ new Set() : visibleSeen(store.ids, stamps, options.ttlMs),
|
|
2422
|
+
path,
|
|
2423
|
+
stamps,
|
|
2424
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
function visibleSeen(ids, stamps, ttlMs) {
|
|
2428
|
+
if (ttlMs === void 0) return ids;
|
|
2429
|
+
if (stamps === null) return /* @__PURE__ */ new Set();
|
|
2430
|
+
const now = Date.now();
|
|
2431
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
2432
|
+
for (const [id, ms] of stamps) if (stampAgeMs(ms, now) <= ttlMs) fresh.add(id);
|
|
2433
|
+
return fresh;
|
|
2434
|
+
}
|
|
2171
2435
|
function filterUnseen(dedup, matches) {
|
|
2172
2436
|
return matches.filter((m) => !dedup.seen.has(m.id));
|
|
2173
2437
|
}
|
|
2174
2438
|
function commitSeen(dedup, returnedIds) {
|
|
2175
|
-
if (returnedIds.length === 0)
|
|
2439
|
+
if (returnedIds.length === 0) {
|
|
2440
|
+
if (dedup.ttlMs !== void 0 && dedup.stamps !== null) {
|
|
2441
|
+
writeSeenStore(dedup.path, dedup.stamps);
|
|
2442
|
+
}
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (dedup.ttlMs !== void 0 || dedup.stamps !== null) {
|
|
2446
|
+
const now = Date.now();
|
|
2447
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2448
|
+
for (const [id, ms] of dedup.stamps ?? []) {
|
|
2449
|
+
if (dedup.ttlMs === void 0 || now - ms <= dedup.ttlMs) merged.set(id, ms);
|
|
2450
|
+
}
|
|
2451
|
+
for (const id of returnedIds) merged.set(id, now);
|
|
2452
|
+
writeSeenStore(dedup.path, merged);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2176
2455
|
const union3 = new Set(dedup.seen);
|
|
2177
2456
|
for (const id of returnedIds) union3.add(id);
|
|
2178
2457
|
if (union3.size === dedup.seen.size) return;
|
|
2179
|
-
|
|
2180
|
-
mkdirSync(dirname(dedup.path), { recursive: true });
|
|
2181
|
-
const tmp = `${dedup.path}.${process.pid}.tmp`;
|
|
2182
|
-
writeFileSync(tmp, JSON.stringify([...union3]), "utf8");
|
|
2183
|
-
renameSync(tmp, dedup.path);
|
|
2184
|
-
} catch {
|
|
2185
|
-
}
|
|
2458
|
+
writeSeenStore(dedup.path, [...union3]);
|
|
2186
2459
|
}
|
|
2187
2460
|
|
|
2188
2461
|
// src/lessons/recall.ts
|
|
@@ -2191,6 +2464,7 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2191
2464
|
await maybeAutoMigrateLessons(projectRoot);
|
|
2192
2465
|
} catch {
|
|
2193
2466
|
}
|
|
2467
|
+
const preReadStamp = currentGraphStamp(projectRoot);
|
|
2194
2468
|
const load = loadLessonsGraphResilient(projectRoot);
|
|
2195
2469
|
if (load.status === "corrupt") {
|
|
2196
2470
|
return { lessons: [], totalMatches: 0, suppressed: 0, corrupt: true };
|
|
@@ -2200,12 +2474,14 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2200
2474
|
}
|
|
2201
2475
|
if (load.status === "absent") return { lessons: [], totalMatches: 0, suppressed: 0 };
|
|
2202
2476
|
const graph = load.graph;
|
|
2477
|
+
refreshCommandFastpath(projectRoot, graph, preReadStamp);
|
|
2203
2478
|
const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
|
|
2204
2479
|
const matches = queryLessons(graph, matchQuery);
|
|
2205
2480
|
const dedup = openSessionDedup({
|
|
2206
2481
|
explicit: options.sessionId,
|
|
2207
2482
|
disabled: options.noDedup,
|
|
2208
|
-
projectRoot
|
|
2483
|
+
projectRoot,
|
|
2484
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
2209
2485
|
});
|
|
2210
2486
|
const forRank = dedup === null ? matches : filterUnseen(dedup, matches);
|
|
2211
2487
|
const cfg = loadRecallConfig(projectRoot);
|
|
@@ -2213,8 +2489,10 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2213
2489
|
limit: options.limit ?? cfg.limit,
|
|
2214
2490
|
maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens,
|
|
2215
2491
|
// Down-rank proven fire-but-fail lessons (empty ⇒ neutral, so recall is
|
|
2216
|
-
// unchanged until the outcome log has real signal). Read from the side-channel
|
|
2217
|
-
|
|
2492
|
+
// unchanged until the outcome log has real signal). Read from the side-channel
|
|
2493
|
+
// only when something survived matching+dedup — a no-match recall must not pay
|
|
2494
|
+
// the (up to 2MB) outcome-log read for a ranking of nothing.
|
|
2495
|
+
effectiveness: forRank.length === 0 ? /* @__PURE__ */ new Map() : loadEffectiveness(projectRoot)
|
|
2218
2496
|
});
|
|
2219
2497
|
if (dedup !== null)
|
|
2220
2498
|
commitSeen(
|
|
@@ -2566,9 +2844,9 @@ var LINE_REFS = String.raw`L\d+(?:\s*,\s*L\d+)*`;
|
|
|
2566
2844
|
var LINE_REF_PATTERNS = [
|
|
2567
2845
|
new RegExp(String.raw`\s*\bSee\s+${LINE_REFS}\.?`, "g"),
|
|
2568
2846
|
// " See L128." / " See L140, L149"
|
|
2569
|
-
new RegExp(String.raw`\s*\((?:${LINE_REFS})\)
|
|
2847
|
+
new RegExp(String.raw`\s*\((?:${LINE_REFS})\)`, "g"),
|
|
2570
2848
|
// " (L174)" / " (L92, L163)"
|
|
2571
|
-
new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]
|
|
2849
|
+
new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]`, "g")
|
|
2572
2850
|
// " [L161, L208]"
|
|
2573
2851
|
];
|
|
2574
2852
|
var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
|
|
@@ -2806,6 +3084,32 @@ function isCoveredByExisting(candidate, existing) {
|
|
|
2806
3084
|
}
|
|
2807
3085
|
return false;
|
|
2808
3086
|
}
|
|
3087
|
+
var OPENER = /^---[ \t]*\r?\n/;
|
|
3088
|
+
var CLOSER = /^---[ \t]*\r?$/m;
|
|
3089
|
+
function splitFrontmatter(content) {
|
|
3090
|
+
const opener = OPENER.exec(content);
|
|
3091
|
+
if (opener === null) return null;
|
|
3092
|
+
const yamlStart = opener[0].length;
|
|
3093
|
+
const closer = CLOSER.exec(content.slice(yamlStart));
|
|
3094
|
+
if (closer === null) return null;
|
|
3095
|
+
const closeStart = yamlStart + closer.index;
|
|
3096
|
+
const closeEnd = closeStart + closer[0].length;
|
|
3097
|
+
return {
|
|
3098
|
+
yaml: content.slice(yamlStart, closeStart),
|
|
3099
|
+
body: content.slice(closeEnd).trim(),
|
|
3100
|
+
prefix: content.slice(0, closeEnd)
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
function serializeFrontmatter(frontmatter, body) {
|
|
3104
|
+
const keys = Object.keys(frontmatter);
|
|
3105
|
+
if (keys.length === 0) return body;
|
|
3106
|
+
const yamlStr = stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
3107
|
+
return `---
|
|
3108
|
+
${yamlStr}
|
|
3109
|
+
---
|
|
3110
|
+
|
|
3111
|
+
${body}`;
|
|
3112
|
+
}
|
|
2809
3113
|
var LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
|
|
2810
3114
|
var LESSONS_CONTRACT_END = "<!-- agentsmesh:lessons-contract:end -->";
|
|
2811
3115
|
function escapeRegExp(value) {
|
|
@@ -2818,10 +3122,8 @@ function stripManagedBlock(content, start, end) {
|
|
|
2818
3122
|
return content.replace(managedBlockPattern(start, end), "").trim();
|
|
2819
3123
|
}
|
|
2820
3124
|
function splitFrontmatterPrefix(content) {
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
if (close === -1) return { prefix: "", body: content.trim() };
|
|
2824
|
-
return { prefix: content.slice(0, close + 3), body: content.slice(close + 3).trim() };
|
|
3125
|
+
const split = splitFrontmatter(content);
|
|
3126
|
+
return split === null ? { prefix: "", body: content.trim() } : { prefix: split.prefix, body: split.body };
|
|
2825
3127
|
}
|
|
2826
3128
|
function insertAtBodyTop(content, block) {
|
|
2827
3129
|
const { prefix, body } = splitFrontmatterPrefix(content);
|
|
@@ -2835,6 +3137,17 @@ ${placed}` : placed;
|
|
|
2835
3137
|
|
|
2836
3138
|
// src/targets/projection/lessons-paragraph.ts
|
|
2837
3139
|
var LEGACY_RAW_FORMS = [
|
|
3140
|
+
// Pre-`--session auto` wording (2026-07): strip sentinel-less copies so a
|
|
3141
|
+
// project scaffolded before the dedup correlator dedups on the next scaffold.
|
|
3142
|
+
`## Lessons (BLOCKING)
|
|
3143
|
+
|
|
3144
|
+
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
3145
|
+
|
|
3146
|
+
**Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
|
|
3147
|
+
|
|
3148
|
+
**Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
|
|
3149
|
+
|
|
3150
|
+
**Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`,
|
|
2838
3151
|
`## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
|
|
2839
3152
|
|
|
2840
3153
|
Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
|
|
@@ -2864,16 +3177,6 @@ ${rule}`, "").replace(rule, ""),
|
|
|
2864
3177
|
content
|
|
2865
3178
|
);
|
|
2866
3179
|
}
|
|
2867
|
-
function serializeFrontmatter(frontmatter, body) {
|
|
2868
|
-
const keys = Object.keys(frontmatter);
|
|
2869
|
-
if (keys.length === 0) return body;
|
|
2870
|
-
const yamlStr = stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
2871
|
-
return `---
|
|
2872
|
-
${yamlStr}
|
|
2873
|
-
---
|
|
2874
|
-
|
|
2875
|
-
${body}`;
|
|
2876
|
-
}
|
|
2877
3180
|
|
|
2878
3181
|
// src/lessons/skill.ts
|
|
2879
3182
|
var LESSONS_SKILL_NAME = "lessons";
|
|
@@ -2892,14 +3195,18 @@ regression / wrong assumption / surprise and you have not captured (nor stated
|
|
|
2892
3195
|
|
|
2893
3196
|
## Recall \u2014 before each edit/command, and at task start
|
|
2894
3197
|
|
|
2895
|
-
\`agentsmesh lessons query --file <path> --cmd <command
|
|
2896
|
-
Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
|
|
3198
|
+
\`agentsmesh lessons query --file <path> --cmd <command> --session auto\`, then apply every
|
|
3199
|
+
rule. Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
|
|
2897
3200
|
for a specific edit is the anti-pattern** \u2014 anchor those to \`--file\`/\`--cmd\`. But at the
|
|
2898
3201
|
START of a task (or when planning), run \`agentsmesh lessons query --keyword "<the task's
|
|
2899
|
-
key terms>" --always\`: that surfaces the conceptual rules no file/command
|
|
2900
|
-
universal always-on lessons \u2014 the manual equivalent of the automatic prompt
|
|
2901
|
-
hook-capable tools.
|
|
2902
|
-
so
|
|
3202
|
+
key terms>" --always --session auto\`: that surfaces the conceptual rules no file/command
|
|
3203
|
+
names PLUS the universal always-on lessons \u2014 the manual equivalent of the automatic prompt
|
|
3204
|
+
recall on hook-capable tools. **Always pass \`--session auto\`**: it suppresses rules already
|
|
3205
|
+
shown this session so repeat recalls stay quiet (without it every recall re-delivers the
|
|
3206
|
+
whole matched set); \`--no-dedup\` re-shows everything after a context reset. Author a
|
|
3207
|
+
\`keyword\` trigger beside a \`file_glob\` on conceptual lessons so they are reachable both
|
|
3208
|
+
ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`; session dedup
|
|
3209
|
+
is on by default there \u2014 \`no_dedup:true\` to re-show).
|
|
2903
3210
|
|
|
2904
3211
|
## Capture \u2014 Gate Function (before any completion claim)
|
|
2905
3212
|
|