agentsmesh 0.30.2 → 0.32.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 +356 -0
- package/README.md +17 -17
- package/dist/canonical.js +3592 -1904
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +222 -208
- package/dist/engine.d.ts +24 -1
- package/dist/engine.js +3844 -2033
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4165 -2353
- package/dist/index.js.map +1 -1
- package/dist/{init-PvpXanVd.d.ts → init-CrZhoNTj.d.ts} +74 -74
- package/dist/lessons.d.ts +8 -4
- package/dist/lessons.js +687 -588
- package/dist/lessons.js.map +1 -1
- package/dist/targets.js +3574 -1886
- package/dist/targets.js.map +1 -1
- package/package.json +6 -2
package/dist/lessons.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { createHash } from 'crypto';
|
|
3
|
-
import picomatch from 'picomatch';
|
|
4
2
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync, statSync } from 'fs';
|
|
5
|
-
import { resolve, join, relative, sep,
|
|
3
|
+
import { resolve, dirname, join, relative, sep, basename, extname } from 'path';
|
|
6
4
|
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import picomatch from 'picomatch';
|
|
7
7
|
import { mkdir, rm, writeFile, readFile, stat, lstat, unlink, rename, chmod } from 'fs/promises';
|
|
8
8
|
import { tmpdir, hostname } from 'os';
|
|
9
9
|
import 'timers/promises';
|
|
@@ -52,6 +52,157 @@ var LessonsGraphSchema = z.object({
|
|
|
52
52
|
function parseGraph(raw) {
|
|
53
53
|
return LessonsGraphSchema.parse(raw);
|
|
54
54
|
}
|
|
55
|
+
var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
|
|
56
|
+
function graphFilePath(projectRoot) {
|
|
57
|
+
return resolve(projectRoot, GRAPH_REL_PATH);
|
|
58
|
+
}
|
|
59
|
+
function loadLessonsGraph(projectRoot) {
|
|
60
|
+
const raw = readFileSync(graphFilePath(projectRoot), "utf8");
|
|
61
|
+
return parseGraph(JSON.parse(raw));
|
|
62
|
+
}
|
|
63
|
+
function tryLoadLessonsGraph(projectRoot) {
|
|
64
|
+
if (!existsSync(graphFilePath(projectRoot))) return null;
|
|
65
|
+
return loadLessonsGraph(projectRoot);
|
|
66
|
+
}
|
|
67
|
+
function loadLessonsGraphResilient(projectRoot) {
|
|
68
|
+
const path = graphFilePath(projectRoot);
|
|
69
|
+
if (!existsSync(path)) return { status: "absent", graph: null };
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
72
|
+
const version = parsed?.version;
|
|
73
|
+
if (typeof version === "number" && version > CURRENT_GRAPH_VERSION) {
|
|
74
|
+
return { status: "newer-version", graph: null, version };
|
|
75
|
+
}
|
|
76
|
+
return { status: "ok", graph: parseGraph(parsed) };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
status: "corrupt",
|
|
80
|
+
graph: null,
|
|
81
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function saveLessonsGraph(projectRoot, graph) {
|
|
86
|
+
const path = graphFilePath(projectRoot);
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
89
|
+
writeFileSync(tmp, serializeGraph(graph), "utf8");
|
|
90
|
+
renameSync(tmp, path);
|
|
91
|
+
}
|
|
92
|
+
function serializeGraph(graph) {
|
|
93
|
+
return `${JSON.stringify(canonicalize(graph), null, 2)}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function canonicalize(value) {
|
|
97
|
+
if (value === null) return null;
|
|
98
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
99
|
+
if (typeof value === "object") {
|
|
100
|
+
const entries = Object.entries(value).sort(
|
|
101
|
+
([a], [b]) => a < b ? -1 : 1
|
|
102
|
+
);
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of entries) out[k] = canonicalize(v);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
var LegacyTriggersSchema = z.object({
|
|
110
|
+
file_globs: z.array(z.string()),
|
|
111
|
+
command_patterns: z.array(z.string()),
|
|
112
|
+
keywords: z.array(z.string())
|
|
113
|
+
}).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
|
|
114
|
+
message: "cluster must declare at least one trigger of any type"
|
|
115
|
+
});
|
|
116
|
+
var LegacyClusterSchema = z.object({
|
|
117
|
+
topic: z.string().regex(/^[a-z0-9-]+$/),
|
|
118
|
+
file: z.string().regex(/\.md$/),
|
|
119
|
+
summary: z.string().min(1),
|
|
120
|
+
triggers: LegacyTriggersSchema
|
|
121
|
+
});
|
|
122
|
+
var LegacyIndexSchema = z.object({
|
|
123
|
+
version: z.literal(1),
|
|
124
|
+
clusters: z.array(LegacyClusterSchema)
|
|
125
|
+
});
|
|
126
|
+
function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
|
|
127
|
+
const specs = [
|
|
128
|
+
...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
|
|
129
|
+
...cluster.triggers.command_patterns.map(
|
|
130
|
+
(p) => ({ kind: "command_pattern", pattern: p })
|
|
131
|
+
),
|
|
132
|
+
...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
|
|
133
|
+
];
|
|
134
|
+
const ids = [];
|
|
135
|
+
for (const spec of specs) {
|
|
136
|
+
const key = `${spec.kind}|${spec.pattern}`;
|
|
137
|
+
let id = triggerIdByKey.get(key);
|
|
138
|
+
if (id === void 0) {
|
|
139
|
+
id = makeTriggerId(spec);
|
|
140
|
+
triggerIdByKey.set(key, id);
|
|
141
|
+
triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
|
|
142
|
+
}
|
|
143
|
+
if (!ids.includes(id)) ids.push(id);
|
|
144
|
+
}
|
|
145
|
+
return ids;
|
|
146
|
+
}
|
|
147
|
+
var TRIGGER_PREFIX = {
|
|
148
|
+
file_glob: "glob",
|
|
149
|
+
command_pattern: "cmd",
|
|
150
|
+
keyword: "kw"
|
|
151
|
+
};
|
|
152
|
+
function makeTriggerId(spec) {
|
|
153
|
+
const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
|
|
154
|
+
return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
|
|
155
|
+
}
|
|
156
|
+
var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
|
|
157
|
+
var NEXT_HEADING_RE = /^##\s+/;
|
|
158
|
+
var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
|
|
159
|
+
var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
|
|
160
|
+
var EVIDENCE_REF_RE = /L\d+/g;
|
|
161
|
+
function parseRulesSection(markdown) {
|
|
162
|
+
const lines = markdown.split(/\r?\n/);
|
|
163
|
+
let inRules = false;
|
|
164
|
+
const rules = [];
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (!inRules) {
|
|
167
|
+
if (RULE_HEADING_RE.test(line)) inRules = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (NEXT_HEADING_RE.test(line)) break;
|
|
171
|
+
const m = RULE_LINE_RE.exec(line);
|
|
172
|
+
if (m === null) continue;
|
|
173
|
+
const ruleIndex = Number(m[1]);
|
|
174
|
+
let body = m[2];
|
|
175
|
+
const evidence = [];
|
|
176
|
+
let tail = EVIDENCE_TAIL_RE.exec(body);
|
|
177
|
+
while (tail !== null) {
|
|
178
|
+
const refs = tail[1];
|
|
179
|
+
const matches = refs.match(EVIDENCE_REF_RE);
|
|
180
|
+
if (matches !== null) evidence.unshift(...matches);
|
|
181
|
+
body = body.slice(0, tail.index).trimEnd();
|
|
182
|
+
tail = EVIDENCE_TAIL_RE.exec(body);
|
|
183
|
+
}
|
|
184
|
+
rules.push({ index: ruleIndex, body, evidence });
|
|
185
|
+
}
|
|
186
|
+
return rules;
|
|
187
|
+
}
|
|
188
|
+
var LEGACY_ARTIFACT_REL = [
|
|
189
|
+
"index.yaml",
|
|
190
|
+
"journal.md",
|
|
191
|
+
"journal.legacy.md",
|
|
192
|
+
"topics",
|
|
193
|
+
"distill-ledger.yaml",
|
|
194
|
+
"distill-proposal.md"
|
|
195
|
+
];
|
|
196
|
+
function deleteLegacyArtifacts(baseDir) {
|
|
197
|
+
const deleted = [];
|
|
198
|
+
for (const rel of LEGACY_ARTIFACT_REL) {
|
|
199
|
+
const abs = join(baseDir, rel);
|
|
200
|
+
if (!existsSync(abs)) continue;
|
|
201
|
+
rmSync(abs, { recursive: true, force: true });
|
|
202
|
+
deleted.push(abs);
|
|
203
|
+
}
|
|
204
|
+
return deleted;
|
|
205
|
+
}
|
|
55
206
|
function normalizeRule(rule) {
|
|
56
207
|
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
57
208
|
}
|
|
@@ -85,7 +236,7 @@ function mergeTriggers(graph, spec) {
|
|
|
85
236
|
if (!triggerIds.includes(existing)) triggerIds.push(existing);
|
|
86
237
|
continue;
|
|
87
238
|
}
|
|
88
|
-
const id =
|
|
239
|
+
const id = makeTriggerId2(spec2);
|
|
89
240
|
graph.triggers[id] = { kind: spec2.kind, pattern: spec2.pattern };
|
|
90
241
|
reverseLookup.set(key, id);
|
|
91
242
|
triggerIds.push(id);
|
|
@@ -96,14 +247,14 @@ function mergeTriggers(graph, spec) {
|
|
|
96
247
|
function triggerKey(t) {
|
|
97
248
|
return `${t.kind}|${t.pattern}`;
|
|
98
249
|
}
|
|
99
|
-
var
|
|
250
|
+
var TRIGGER_PREFIX2 = {
|
|
100
251
|
file_glob: "glob",
|
|
101
252
|
command_pattern: "cmd",
|
|
102
253
|
keyword: "kw"
|
|
103
254
|
};
|
|
104
|
-
function
|
|
255
|
+
function makeTriggerId2(spec) {
|
|
105
256
|
const hash = createHash("sha1").update(triggerKey(spec)).digest("hex").slice(0, 8);
|
|
106
|
-
return `t-${
|
|
257
|
+
return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
|
|
107
258
|
}
|
|
108
259
|
function makeLessonId(graph, topic, ruleKey) {
|
|
109
260
|
const slug = ruleToSlug(ruleKey);
|
|
@@ -397,338 +548,32 @@ function jaccard(a, b) {
|
|
|
397
548
|
for (const t of a) if (b.has(t)) intersection += 1;
|
|
398
549
|
return intersection / (a.size + b.size - intersection);
|
|
399
550
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
function tryLoadLessonsGraph(projectRoot) {
|
|
409
|
-
if (!existsSync(graphFilePath(projectRoot))) return null;
|
|
410
|
-
return loadLessonsGraph(projectRoot);
|
|
411
|
-
}
|
|
412
|
-
function loadLessonsGraphResilient(projectRoot) {
|
|
413
|
-
const path = graphFilePath(projectRoot);
|
|
414
|
-
if (!existsSync(path)) return { status: "absent", graph: null };
|
|
415
|
-
try {
|
|
416
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
417
|
-
const version = parsed?.version;
|
|
418
|
-
if (typeof version === "number" && version > CURRENT_GRAPH_VERSION) {
|
|
419
|
-
return { status: "newer-version", graph: null, version };
|
|
420
|
-
}
|
|
421
|
-
return { status: "ok", graph: parseGraph(parsed) };
|
|
422
|
-
} catch (error) {
|
|
423
|
-
return {
|
|
424
|
-
status: "corrupt",
|
|
425
|
-
graph: null,
|
|
426
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
427
|
-
};
|
|
551
|
+
|
|
552
|
+
// src/core/errors.ts
|
|
553
|
+
var AgentsMeshError = class extends Error {
|
|
554
|
+
code;
|
|
555
|
+
constructor(code, message, options) {
|
|
556
|
+
super(message, options);
|
|
557
|
+
this.name = "AgentsMeshError";
|
|
558
|
+
this.code = code;
|
|
428
559
|
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
function canonicalize(value) {
|
|
442
|
-
if (value === null) return null;
|
|
443
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
444
|
-
if (typeof value === "object") {
|
|
445
|
-
const entries = Object.entries(value).sort(
|
|
446
|
-
([a], [b]) => a < b ? -1 : 1
|
|
560
|
+
};
|
|
561
|
+
var LockAcquisitionError = class extends AgentsMeshError {
|
|
562
|
+
lockPath;
|
|
563
|
+
holder;
|
|
564
|
+
/** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
|
|
565
|
+
label;
|
|
566
|
+
constructor(lockPath, holder, options) {
|
|
567
|
+
const label = options?.label ?? "lock";
|
|
568
|
+
super(
|
|
569
|
+
"AM_LOCK_ACQUISITION_FAILED",
|
|
570
|
+
`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.`,
|
|
571
|
+
options
|
|
447
572
|
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return value;
|
|
453
|
-
}
|
|
454
|
-
var LegacyTriggersSchema = z.object({
|
|
455
|
-
file_globs: z.array(z.string()),
|
|
456
|
-
command_patterns: z.array(z.string()),
|
|
457
|
-
keywords: z.array(z.string())
|
|
458
|
-
}).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
|
|
459
|
-
message: "cluster must declare at least one trigger of any type"
|
|
460
|
-
});
|
|
461
|
-
var LegacyClusterSchema = z.object({
|
|
462
|
-
topic: z.string().regex(/^[a-z0-9-]+$/),
|
|
463
|
-
file: z.string().regex(/\.md$/),
|
|
464
|
-
summary: z.string().min(1),
|
|
465
|
-
triggers: LegacyTriggersSchema
|
|
466
|
-
});
|
|
467
|
-
var LegacyIndexSchema = z.object({
|
|
468
|
-
version: z.literal(1),
|
|
469
|
-
clusters: z.array(LegacyClusterSchema)
|
|
470
|
-
});
|
|
471
|
-
function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
|
|
472
|
-
const specs = [
|
|
473
|
-
...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
|
|
474
|
-
...cluster.triggers.command_patterns.map(
|
|
475
|
-
(p) => ({ kind: "command_pattern", pattern: p })
|
|
476
|
-
),
|
|
477
|
-
...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
|
|
478
|
-
];
|
|
479
|
-
const ids = [];
|
|
480
|
-
for (const spec of specs) {
|
|
481
|
-
const key = `${spec.kind}|${spec.pattern}`;
|
|
482
|
-
let id = triggerIdByKey.get(key);
|
|
483
|
-
if (id === void 0) {
|
|
484
|
-
id = makeTriggerId2(spec);
|
|
485
|
-
triggerIdByKey.set(key, id);
|
|
486
|
-
triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
|
|
487
|
-
}
|
|
488
|
-
if (!ids.includes(id)) ids.push(id);
|
|
489
|
-
}
|
|
490
|
-
return ids;
|
|
491
|
-
}
|
|
492
|
-
var TRIGGER_PREFIX2 = {
|
|
493
|
-
file_glob: "glob",
|
|
494
|
-
command_pattern: "cmd",
|
|
495
|
-
keyword: "kw"
|
|
496
|
-
};
|
|
497
|
-
function makeTriggerId2(spec) {
|
|
498
|
-
const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
|
|
499
|
-
return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
|
|
500
|
-
}
|
|
501
|
-
var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
|
|
502
|
-
var NEXT_HEADING_RE = /^##\s+/;
|
|
503
|
-
var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
|
|
504
|
-
var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
|
|
505
|
-
var EVIDENCE_REF_RE = /L\d+/g;
|
|
506
|
-
function parseRulesSection(markdown) {
|
|
507
|
-
const lines = markdown.split(/\r?\n/);
|
|
508
|
-
let inRules = false;
|
|
509
|
-
const rules = [];
|
|
510
|
-
for (const line of lines) {
|
|
511
|
-
if (!inRules) {
|
|
512
|
-
if (RULE_HEADING_RE.test(line)) inRules = true;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
if (NEXT_HEADING_RE.test(line)) break;
|
|
516
|
-
const m = RULE_LINE_RE.exec(line);
|
|
517
|
-
if (m === null) continue;
|
|
518
|
-
const ruleIndex = Number(m[1]);
|
|
519
|
-
let body = m[2];
|
|
520
|
-
const evidence = [];
|
|
521
|
-
let tail = EVIDENCE_TAIL_RE.exec(body);
|
|
522
|
-
while (tail !== null) {
|
|
523
|
-
const refs = tail[1];
|
|
524
|
-
const matches = refs.match(EVIDENCE_REF_RE);
|
|
525
|
-
if (matches !== null) evidence.unshift(...matches);
|
|
526
|
-
body = body.slice(0, tail.index).trimEnd();
|
|
527
|
-
tail = EVIDENCE_TAIL_RE.exec(body);
|
|
528
|
-
}
|
|
529
|
-
rules.push({ index: ruleIndex, body, evidence });
|
|
530
|
-
}
|
|
531
|
-
return rules;
|
|
532
|
-
}
|
|
533
|
-
var LEGACY_ARTIFACT_REL = [
|
|
534
|
-
"index.yaml",
|
|
535
|
-
"journal.md",
|
|
536
|
-
"journal.legacy.md",
|
|
537
|
-
"topics",
|
|
538
|
-
"distill-ledger.yaml",
|
|
539
|
-
"distill-proposal.md"
|
|
540
|
-
];
|
|
541
|
-
function deleteLegacyArtifacts(baseDir) {
|
|
542
|
-
const deleted = [];
|
|
543
|
-
for (const rel of LEGACY_ARTIFACT_REL) {
|
|
544
|
-
const abs = join(baseDir, rel);
|
|
545
|
-
if (!existsSync(abs)) continue;
|
|
546
|
-
rmSync(abs, { recursive: true, force: true });
|
|
547
|
-
deleted.push(abs);
|
|
548
|
-
}
|
|
549
|
-
return deleted;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// src/lessons/import-legacy-merge.ts
|
|
553
|
-
async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
|
|
554
|
-
let addedLessons = 0;
|
|
555
|
-
const addedTriggers = /* @__PURE__ */ new Set();
|
|
556
|
-
const touchedTopics = /* @__PURE__ */ new Set();
|
|
557
|
-
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
558
|
-
addedLessons = 0;
|
|
559
|
-
addedTriggers.clear();
|
|
560
|
-
touchedTopics.clear();
|
|
561
|
-
for (const spec of specs) {
|
|
562
|
-
const result = addLessonInto(g, spec, {
|
|
563
|
-
allowNewTopic: true,
|
|
564
|
-
topicSummary: summaryByTopic.get(spec.topic),
|
|
565
|
-
// Legacy lessons may predate the ≥1-trigger requirement; recover them
|
|
566
|
-
// as-is rather than dropping historical knowledge.
|
|
567
|
-
allowNoTrigger: true
|
|
568
|
-
});
|
|
569
|
-
if (result.isNewLesson) addedLessons += 1;
|
|
570
|
-
for (const t of result.newTriggerIds) addedTriggers.add(t);
|
|
571
|
-
touchedTopics.add(spec.topic);
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
575
|
-
return {
|
|
576
|
-
wroteGraphPath: paths.graph,
|
|
577
|
-
deletedPaths,
|
|
578
|
-
topicCount: touchedTopics.size,
|
|
579
|
-
lessonCount: addedLessons,
|
|
580
|
-
triggerCount: addedTriggers.size
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
var BASE_REL = ".agentsmesh/lessons";
|
|
584
|
-
function lessonsPaths(projectRoot) {
|
|
585
|
-
const base = join(projectRoot, BASE_REL);
|
|
586
|
-
return {
|
|
587
|
-
base,
|
|
588
|
-
graph: join(base, "lessons.json"),
|
|
589
|
-
config: join(base, "config.json"),
|
|
590
|
-
journal: join(base, "journal.md"),
|
|
591
|
-
index: join(base, "index.yaml"),
|
|
592
|
-
topicsDir: join(base, "topics")
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
function toRelPath(projectRoot, absolute) {
|
|
596
|
-
return relative(projectRoot, absolute).split(sep).join("/");
|
|
597
|
-
}
|
|
598
|
-
var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
599
|
-
|
|
600
|
-
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
601
|
-
|
|
602
|
-
**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.
|
|
603
|
-
|
|
604
|
-
**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>\`.
|
|
605
|
-
|
|
606
|
-
**Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
|
|
607
|
-
|
|
608
|
-
// src/lessons/import-legacy.ts
|
|
609
|
-
var LessonsGraphExistsError = class extends Error {
|
|
610
|
-
code = "LESSONS_GRAPH_EXISTS";
|
|
611
|
-
constructor() {
|
|
612
|
-
super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
|
|
613
|
-
this.name = "LessonsGraphExistsError";
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
async function importLegacyLessons(projectRoot, options) {
|
|
617
|
-
const paths = lessonsPaths(projectRoot);
|
|
618
|
-
const indexRaw = readFileSync(paths.index, "utf8");
|
|
619
|
-
const index = LegacyIndexSchema.parse(parse(indexRaw));
|
|
620
|
-
const topics = {};
|
|
621
|
-
const triggersById = /* @__PURE__ */ new Map();
|
|
622
|
-
const triggerIdByKey = /* @__PURE__ */ new Map();
|
|
623
|
-
const lessons = {};
|
|
624
|
-
const specs = [];
|
|
625
|
-
const summaryByTopic = /* @__PURE__ */ new Map();
|
|
626
|
-
for (const cluster of index.clusters) {
|
|
627
|
-
topics[cluster.topic] = { summary: cluster.summary };
|
|
628
|
-
summaryByTopic.set(cluster.topic, cluster.summary);
|
|
629
|
-
const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
|
|
630
|
-
const topicFile = join(projectRoot, cluster.file);
|
|
631
|
-
if (!existsSync(topicFile)) {
|
|
632
|
-
throw new Error(
|
|
633
|
-
`importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
const topicMarkdown = readFileSync(topicFile, "utf8");
|
|
637
|
-
for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
|
|
638
|
-
const lessonEvidence = [
|
|
639
|
-
`legacy:${cluster.file}#rule-${ruleIndex}`,
|
|
640
|
-
...evidence.map((e) => `legacy:${e}`)
|
|
641
|
-
];
|
|
642
|
-
lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
|
|
643
|
-
rule: body,
|
|
644
|
-
topics: [cluster.topic],
|
|
645
|
-
triggers: clusterTriggerIds,
|
|
646
|
-
evidence: lessonEvidence,
|
|
647
|
-
status: "active",
|
|
648
|
-
createdAt: options.migratedAt
|
|
649
|
-
};
|
|
650
|
-
specs.push({
|
|
651
|
-
rule: body,
|
|
652
|
-
topic: cluster.topic,
|
|
653
|
-
triggers: {
|
|
654
|
-
files: cluster.triggers.file_globs,
|
|
655
|
-
commands: cluster.triggers.command_patterns,
|
|
656
|
-
keywords: cluster.triggers.keywords
|
|
657
|
-
},
|
|
658
|
-
evidence: lessonEvidence,
|
|
659
|
-
createdAt: options.migratedAt
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (options.merge === true)
|
|
664
|
-
return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
|
|
665
|
-
const triggers = Object.fromEntries(triggersById.entries());
|
|
666
|
-
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
667
|
-
const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
|
|
668
|
-
if (options.force !== true && populated) {
|
|
669
|
-
throw new LessonsGraphExistsError();
|
|
670
|
-
}
|
|
671
|
-
g.version = CURRENT_GRAPH_VERSION;
|
|
672
|
-
g.lessons = lessons;
|
|
673
|
-
g.topics = topics;
|
|
674
|
-
g.triggers = triggers;
|
|
675
|
-
});
|
|
676
|
-
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
677
|
-
return {
|
|
678
|
-
wroteGraphPath: paths.graph,
|
|
679
|
-
deletedPaths,
|
|
680
|
-
topicCount: Object.keys(topics).length,
|
|
681
|
-
lessonCount: Object.keys(lessons).length,
|
|
682
|
-
triggerCount: triggersById.size
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/lessons/auto-migrate.ts
|
|
687
|
-
function todayIso2() {
|
|
688
|
-
const now = /* @__PURE__ */ new Date();
|
|
689
|
-
const y = now.getUTCFullYear();
|
|
690
|
-
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
691
|
-
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
692
|
-
return `${y}-${m}-${d}`;
|
|
693
|
-
}
|
|
694
|
-
async function maybeAutoMigrateLessons(projectRoot) {
|
|
695
|
-
if (existsSync(graphFilePath(projectRoot))) return false;
|
|
696
|
-
const paths = lessonsPaths(projectRoot);
|
|
697
|
-
if (!existsSync(paths.index)) return false;
|
|
698
|
-
try {
|
|
699
|
-
await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
|
|
700
|
-
return true;
|
|
701
|
-
} catch (err) {
|
|
702
|
-
if (err instanceof LessonsGraphExistsError) return false;
|
|
703
|
-
throw err;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// src/core/errors.ts
|
|
708
|
-
var AgentsMeshError = class extends Error {
|
|
709
|
-
code;
|
|
710
|
-
constructor(code, message, options) {
|
|
711
|
-
super(message, options);
|
|
712
|
-
this.name = "AgentsMeshError";
|
|
713
|
-
this.code = code;
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
var LockAcquisitionError = class extends AgentsMeshError {
|
|
717
|
-
lockPath;
|
|
718
|
-
holder;
|
|
719
|
-
/** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
|
|
720
|
-
label;
|
|
721
|
-
constructor(lockPath, holder, options) {
|
|
722
|
-
const label = options?.label ?? "lock";
|
|
723
|
-
super(
|
|
724
|
-
"AM_LOCK_ACQUISITION_FAILED",
|
|
725
|
-
`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.`,
|
|
726
|
-
options
|
|
727
|
-
);
|
|
728
|
-
this.name = "LockAcquisitionError";
|
|
729
|
-
this.lockPath = lockPath;
|
|
730
|
-
this.holder = holder;
|
|
731
|
-
this.label = label;
|
|
573
|
+
this.name = "LockAcquisitionError";
|
|
574
|
+
this.lockPath = lockPath;
|
|
575
|
+
this.holder = holder;
|
|
576
|
+
this.label = label;
|
|
732
577
|
}
|
|
733
578
|
};
|
|
734
579
|
var FileSystemError = class extends AgentsMeshError {
|
|
@@ -1781,239 +1626,185 @@ function addLessonInto(graph, input, options) {
|
|
|
1781
1626
|
const id = makeLessonId(graph, input.topic, ruleKey);
|
|
1782
1627
|
graph.lessons[id] = {
|
|
1783
1628
|
rule: trimmedRule,
|
|
1784
|
-
topics: [input.topic],
|
|
1785
|
-
triggers: triggerIds,
|
|
1786
|
-
evidence: input.evidence === void 0 ? [] : [...input.evidence],
|
|
1787
|
-
status: "active",
|
|
1788
|
-
createdAt: input.createdAt ?? todayIso(),
|
|
1789
|
-
...input.rationale === void 0 ? {} : { rationale: input.rationale },
|
|
1790
|
-
...input.scope === "always" ? { scope: "always" } : {}
|
|
1791
|
-
};
|
|
1792
|
-
const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
|
|
1793
|
-
const nearDup = nearDuplicateWarning(graph, id);
|
|
1794
|
-
return {
|
|
1795
|
-
id,
|
|
1796
|
-
isNewLesson: true,
|
|
1797
|
-
isNewTopic,
|
|
1798
|
-
newTriggerIds,
|
|
1799
|
-
warnings: nearDup === null ? warnings : [...warnings, nearDup]
|
|
1800
|
-
};
|
|
1801
|
-
}
|
|
1802
|
-
function findExistingLessonByRule(graph, ruleKey) {
|
|
1803
|
-
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1804
|
-
if (lesson.status !== "active") continue;
|
|
1805
|
-
if (normalizeRule(lesson.rule) === ruleKey) return id;
|
|
1806
|
-
}
|
|
1807
|
-
return null;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
// src/lessons/ranking-signals.ts
|
|
1811
|
-
function buildFanout(graph) {
|
|
1812
|
-
const fanout = /* @__PURE__ */ new Map();
|
|
1813
|
-
for (const lesson of Object.values(graph.lessons)) {
|
|
1814
|
-
if (lesson.status !== "active") continue;
|
|
1815
|
-
for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
|
|
1816
|
-
}
|
|
1817
|
-
return fanout;
|
|
1818
|
-
}
|
|
1819
|
-
function buildTopicCoherence(matches) {
|
|
1820
|
-
const topicCount = /* @__PURE__ */ new Map();
|
|
1821
|
-
for (const { lesson } of matches) {
|
|
1822
|
-
for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
|
|
1823
|
-
}
|
|
1824
|
-
const coherence = /* @__PURE__ */ new Map();
|
|
1825
|
-
for (const { id, lesson } of matches) {
|
|
1826
|
-
let best = 0;
|
|
1827
|
-
for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
|
|
1828
|
-
coherence.set(id, best);
|
|
1829
|
-
}
|
|
1830
|
-
return coherence;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
// src/lessons/prune.ts
|
|
1834
|
-
function planPrune(graph, options = {}) {
|
|
1835
|
-
const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
|
|
1836
|
-
const fanout = buildFanout(graph);
|
|
1837
|
-
const trimmedLessons = [];
|
|
1838
|
-
const keptByLesson = /* @__PURE__ */ new Map();
|
|
1839
|
-
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1840
|
-
if (lesson.status !== "active") continue;
|
|
1841
|
-
if (options.trimOverCap === false || lesson.triggers.length <= cap) {
|
|
1842
|
-
keptByLesson.set(id, lesson.triggers);
|
|
1843
|
-
continue;
|
|
1844
|
-
}
|
|
1845
|
-
const ordered = [...lesson.triggers].sort((a, b) => {
|
|
1846
|
-
const fa = fanout.get(a);
|
|
1847
|
-
const fb = fanout.get(b);
|
|
1848
|
-
return fa !== fb ? fa - fb : a < b ? -1 : 1;
|
|
1849
|
-
});
|
|
1850
|
-
const drop = new Set(ordered.slice(cap));
|
|
1851
|
-
const kept = lesson.triggers.filter((t) => !drop.has(t));
|
|
1852
|
-
keptByLesson.set(id, kept);
|
|
1853
|
-
trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
|
|
1854
|
-
}
|
|
1855
|
-
const removedDeadGlobs = [];
|
|
1856
|
-
const unreachableLessons = [];
|
|
1857
|
-
if (options.knownPaths !== void 0) {
|
|
1858
|
-
const dead = deadFileGlobIds(graph, options.knownPaths);
|
|
1859
|
-
if (dead.size > 0) {
|
|
1860
|
-
for (const [id, kept] of keptByLesson) {
|
|
1861
|
-
const deadInLesson = kept.filter((t) => dead.has(t));
|
|
1862
|
-
if (deadInLesson.length === 0) continue;
|
|
1863
|
-
const remaining = kept.filter((t) => !dead.has(t));
|
|
1864
|
-
if (remaining.length >= 1) {
|
|
1865
|
-
keptByLesson.set(id, remaining);
|
|
1866
|
-
removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
|
|
1867
|
-
} else {
|
|
1868
|
-
unreachableLessons.push(id);
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
unreachableLessons.sort();
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
const live = /* @__PURE__ */ new Set();
|
|
1875
|
-
for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
|
|
1876
|
-
const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
|
|
1877
|
-
const referencedTopics = /* @__PURE__ */ new Set();
|
|
1878
|
-
for (const lesson of Object.values(graph.lessons)) {
|
|
1879
|
-
for (const topic of lesson.topics) referencedTopics.add(topic);
|
|
1880
|
-
}
|
|
1881
|
-
const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
|
|
1882
|
-
return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
|
|
1883
|
-
}
|
|
1884
|
-
function applyPruneToGraph(graph, plan) {
|
|
1885
|
-
for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
|
|
1886
|
-
const lesson = graph.lessons[trim.id];
|
|
1887
|
-
if (lesson === void 0) continue;
|
|
1888
|
-
const drop = new Set(trim.removedTriggers);
|
|
1889
|
-
graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
|
|
1890
|
-
}
|
|
1891
|
-
for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
|
|
1892
|
-
const dead = new Set(plan.removedTriggerIds);
|
|
1893
|
-
if (dead.size === 0) return;
|
|
1894
|
-
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1895
|
-
if (lesson.triggers.some((t) => dead.has(t))) {
|
|
1896
|
-
graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
for (const t of dead) delete graph.triggers[t];
|
|
1900
|
-
}
|
|
1901
|
-
function isEmptyPrunePlan(plan) {
|
|
1902
|
-
return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
// src/lessons/auto-prune.ts
|
|
1906
|
-
function isAutoPruneEnabled(projectRoot) {
|
|
1907
|
-
const path = lessonsPaths(projectRoot).config;
|
|
1908
|
-
if (!existsSync(path)) return false;
|
|
1909
|
-
try {
|
|
1910
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1911
|
-
if (typeof parsed !== "object" || parsed === null) return false;
|
|
1912
|
-
return parsed.autoPrune === true;
|
|
1913
|
-
} catch {
|
|
1914
|
-
return false;
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
async function maybeAutoPrune(projectRoot, knownPaths) {
|
|
1918
|
-
if (!isAutoPruneEnabled(projectRoot)) return null;
|
|
1919
|
-
const preview = tryLoadLessonsGraph(projectRoot);
|
|
1920
|
-
if (preview === null) return null;
|
|
1921
|
-
if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
|
|
1922
|
-
let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
|
|
1923
|
-
await mutateLessonsGraph(projectRoot, (graph) => {
|
|
1924
|
-
const plan = planPrune(graph, { trimOverCap: false, knownPaths });
|
|
1925
|
-
if (isEmptyPrunePlan(plan)) return;
|
|
1926
|
-
applyPruneToGraph(graph, plan);
|
|
1927
|
-
summary = {
|
|
1928
|
-
removedTriggers: plan.removedTriggerIds.length,
|
|
1929
|
-
removedTopics: plan.removedTopicIds.length,
|
|
1930
|
-
detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
|
|
1931
|
-
};
|
|
1932
|
-
});
|
|
1933
|
-
const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
|
|
1934
|
-
return total > 0 ? summary : null;
|
|
1935
|
-
}
|
|
1936
|
-
function appendJsonl(path, record, opts) {
|
|
1937
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1938
|
-
appendFileSync(path, `${JSON.stringify(record)}
|
|
1939
|
-
`, "utf8");
|
|
1940
|
-
if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
|
|
1941
|
-
}
|
|
1942
|
-
function capJsonl(path, maxRecords) {
|
|
1943
|
-
if (!existsSync(path)) return;
|
|
1944
|
-
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
1945
|
-
if (lines.length <= maxRecords) return;
|
|
1946
|
-
const kept = lines.slice(lines.length - maxRecords);
|
|
1947
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
1948
|
-
writeFileSync(tmp, `${kept.join("\n")}
|
|
1949
|
-
`, "utf8");
|
|
1950
|
-
renameSync(tmp, path);
|
|
1629
|
+
topics: [input.topic],
|
|
1630
|
+
triggers: triggerIds,
|
|
1631
|
+
evidence: input.evidence === void 0 ? [] : [...input.evidence],
|
|
1632
|
+
status: "active",
|
|
1633
|
+
createdAt: input.createdAt ?? todayIso(),
|
|
1634
|
+
...input.rationale === void 0 ? {} : { rationale: input.rationale },
|
|
1635
|
+
...input.scope === "always" ? { scope: "always" } : {}
|
|
1636
|
+
};
|
|
1637
|
+
const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
|
|
1638
|
+
const nearDup = nearDuplicateWarning(graph, id);
|
|
1639
|
+
return {
|
|
1640
|
+
id,
|
|
1641
|
+
isNewLesson: true,
|
|
1642
|
+
isNewTopic,
|
|
1643
|
+
newTriggerIds,
|
|
1644
|
+
warnings: nearDup === null ? warnings : [...warnings, nearDup]
|
|
1645
|
+
};
|
|
1951
1646
|
}
|
|
1952
|
-
function
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
if (line.trim().length === 0) continue;
|
|
1957
|
-
try {
|
|
1958
|
-
out.push(JSON.parse(line));
|
|
1959
|
-
} catch {
|
|
1960
|
-
}
|
|
1647
|
+
function findExistingLessonByRule(graph, ruleKey) {
|
|
1648
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
1649
|
+
if (lesson.status !== "active") continue;
|
|
1650
|
+
if (normalizeRule(lesson.rule) === ruleKey) return id;
|
|
1961
1651
|
}
|
|
1962
|
-
return
|
|
1652
|
+
return null;
|
|
1963
1653
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1654
|
+
|
|
1655
|
+
// src/lessons/import-legacy-merge.ts
|
|
1656
|
+
async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
|
|
1657
|
+
let addedLessons = 0;
|
|
1658
|
+
const addedTriggers = /* @__PURE__ */ new Set();
|
|
1659
|
+
const touchedTopics = /* @__PURE__ */ new Set();
|
|
1660
|
+
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
1661
|
+
addedLessons = 0;
|
|
1662
|
+
addedTriggers.clear();
|
|
1663
|
+
touchedTopics.clear();
|
|
1664
|
+
for (const spec of specs) {
|
|
1665
|
+
const result = addLessonInto(g, spec, {
|
|
1666
|
+
allowNewTopic: true,
|
|
1667
|
+
topicSummary: summaryByTopic.get(spec.topic),
|
|
1668
|
+
// Legacy lessons may predate the ≥1-trigger requirement; recover them
|
|
1669
|
+
// as-is rather than dropping historical knowledge.
|
|
1670
|
+
allowNoTrigger: true
|
|
1671
|
+
});
|
|
1672
|
+
if (result.isNewLesson) addedLessons += 1;
|
|
1673
|
+
for (const t of result.newTriggerIds) addedTriggers.add(t);
|
|
1674
|
+
touchedTopics.add(spec.topic);
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
1678
|
+
return {
|
|
1679
|
+
wroteGraphPath: paths.graph,
|
|
1680
|
+
deletedPaths,
|
|
1681
|
+
topicCount: touchedTopics.size,
|
|
1682
|
+
lessonCount: addedLessons,
|
|
1683
|
+
triggerCount: addedTriggers.size
|
|
1684
|
+
};
|
|
1971
1685
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1686
|
+
var BASE_REL = ".agentsmesh/lessons";
|
|
1687
|
+
function lessonsPaths(projectRoot) {
|
|
1688
|
+
const base = join(projectRoot, BASE_REL);
|
|
1689
|
+
return {
|
|
1690
|
+
base,
|
|
1691
|
+
graph: join(base, "lessons.json"),
|
|
1692
|
+
config: join(base, "config.json"),
|
|
1693
|
+
journal: join(base, "journal.md"),
|
|
1694
|
+
index: join(base, "index.yaml"),
|
|
1695
|
+
topicsDir: join(base, "topics")
|
|
1696
|
+
};
|
|
1974
1697
|
}
|
|
1975
|
-
function
|
|
1976
|
-
return
|
|
1698
|
+
function toRelPath(projectRoot, absolute) {
|
|
1699
|
+
return relative(projectRoot, absolute).split(sep).join("/");
|
|
1977
1700
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1701
|
+
var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
1702
|
+
|
|
1703
|
+
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
1704
|
+
|
|
1705
|
+
**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.
|
|
1706
|
+
|
|
1707
|
+
**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
|
+
|
|
1709
|
+
**Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
|
|
1710
|
+
|
|
1711
|
+
// src/lessons/import-legacy.ts
|
|
1712
|
+
var LessonsGraphExistsError = class extends Error {
|
|
1713
|
+
code = "LESSONS_GRAPH_EXISTS";
|
|
1714
|
+
constructor() {
|
|
1715
|
+
super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
|
|
1716
|
+
this.name = "LessonsGraphExistsError";
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
async function importLegacyLessons(projectRoot, options) {
|
|
1720
|
+
const paths = lessonsPaths(projectRoot);
|
|
1721
|
+
const indexRaw = readFileSync(paths.index, "utf8");
|
|
1722
|
+
const index = LegacyIndexSchema.parse(parse(indexRaw));
|
|
1723
|
+
const topics = {};
|
|
1724
|
+
const triggersById = /* @__PURE__ */ new Map();
|
|
1725
|
+
const triggerIdByKey = /* @__PURE__ */ new Map();
|
|
1726
|
+
const lessons = {};
|
|
1727
|
+
const specs = [];
|
|
1728
|
+
const summaryByTopic = /* @__PURE__ */ new Map();
|
|
1729
|
+
for (const cluster of index.clusters) {
|
|
1730
|
+
topics[cluster.topic] = { summary: cluster.summary };
|
|
1731
|
+
summaryByTopic.set(cluster.topic, cluster.summary);
|
|
1732
|
+
const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
|
|
1733
|
+
const topicFile = join(projectRoot, cluster.file);
|
|
1734
|
+
if (!existsSync(topicFile)) {
|
|
1735
|
+
throw new Error(
|
|
1736
|
+
`importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
const topicMarkdown = readFileSync(topicFile, "utf8");
|
|
1740
|
+
for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
|
|
1741
|
+
const lessonEvidence = [
|
|
1742
|
+
`legacy:${cluster.file}#rule-${ruleIndex}`,
|
|
1743
|
+
...evidence.map((e) => `legacy:${e}`)
|
|
1744
|
+
];
|
|
1745
|
+
lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
|
|
1746
|
+
rule: body,
|
|
1747
|
+
topics: [cluster.topic],
|
|
1748
|
+
triggers: clusterTriggerIds,
|
|
1749
|
+
evidence: lessonEvidence,
|
|
1750
|
+
status: "active",
|
|
1751
|
+
createdAt: options.migratedAt
|
|
1752
|
+
};
|
|
1753
|
+
specs.push({
|
|
1754
|
+
rule: body,
|
|
1755
|
+
topic: cluster.topic,
|
|
1756
|
+
triggers: {
|
|
1757
|
+
files: cluster.triggers.file_globs,
|
|
1758
|
+
commands: cluster.triggers.command_patterns,
|
|
1759
|
+
keywords: cluster.triggers.keywords
|
|
1760
|
+
},
|
|
1761
|
+
evidence: lessonEvidence,
|
|
1762
|
+
createdAt: options.migratedAt
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (options.merge === true)
|
|
1767
|
+
return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
|
|
1768
|
+
const triggers = Object.fromEntries(triggersById.entries());
|
|
1769
|
+
await mutateLessonsGraphLocked(projectRoot, (g) => {
|
|
1770
|
+
const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
|
|
1771
|
+
if (options.force !== true && populated) {
|
|
1772
|
+
throw new LessonsGraphExistsError();
|
|
1773
|
+
}
|
|
1774
|
+
g.version = CURRENT_GRAPH_VERSION;
|
|
1775
|
+
g.lessons = lessons;
|
|
1776
|
+
g.topics = topics;
|
|
1777
|
+
g.triggers = triggers;
|
|
1983
1778
|
});
|
|
1779
|
+
const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
|
|
1780
|
+
return {
|
|
1781
|
+
wroteGraphPath: paths.graph,
|
|
1782
|
+
deletedPaths,
|
|
1783
|
+
topicCount: Object.keys(topics).length,
|
|
1784
|
+
lessonCount: Object.keys(lessons).length,
|
|
1785
|
+
triggerCount: triggersById.size
|
|
1786
|
+
};
|
|
1984
1787
|
}
|
|
1985
1788
|
|
|
1986
|
-
// src/lessons/
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
if (!isTelemetryEnabled(env)) return;
|
|
1994
|
-
appendJsonl(captureLogPath(projectRoot), record, {
|
|
1995
|
-
maxRecords: MAX_CAPTURE_LOG_RECORDS,
|
|
1996
|
-
trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
|
|
1997
|
-
});
|
|
1789
|
+
// src/lessons/auto-migrate.ts
|
|
1790
|
+
function todayIso2() {
|
|
1791
|
+
const now = /* @__PURE__ */ new Date();
|
|
1792
|
+
const y = now.getUTCFullYear();
|
|
1793
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
1794
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
1795
|
+
return `${y}-${m}-${d}`;
|
|
1998
1796
|
}
|
|
1999
|
-
function
|
|
2000
|
-
if (
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
{
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
blocked: result === null,
|
|
2011
|
-
warningCodes: result?.warnings.map((w) => w.code) ?? [],
|
|
2012
|
-
...session !== void 0 ? { session } : {},
|
|
2013
|
-
...result !== null ? { lessonId: result.id } : {}
|
|
2014
|
-
},
|
|
2015
|
-
env
|
|
2016
|
-
);
|
|
1797
|
+
async function maybeAutoMigrateLessons(projectRoot) {
|
|
1798
|
+
if (existsSync(graphFilePath(projectRoot))) return false;
|
|
1799
|
+
const paths = lessonsPaths(projectRoot);
|
|
1800
|
+
if (!existsSync(paths.index)) return false;
|
|
1801
|
+
try {
|
|
1802
|
+
await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
|
|
1803
|
+
return true;
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
if (err instanceof LessonsGraphExistsError) return false;
|
|
1806
|
+
throw err;
|
|
1807
|
+
}
|
|
2017
1808
|
}
|
|
2018
1809
|
function normalizeRecallFile(file, projectRoot) {
|
|
2019
1810
|
const forward = file.replaceAll("\\", "/");
|
|
@@ -2035,28 +1826,6 @@ function safeRealpath(path) {
|
|
|
2035
1826
|
return resolve(safeRealpath(parent), basename(path));
|
|
2036
1827
|
}
|
|
2037
1828
|
}
|
|
2038
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2039
|
-
var MAX_FILES = 2e5;
|
|
2040
|
-
function listProjectFiles(projectRoot) {
|
|
2041
|
-
const out = /* @__PURE__ */ new Set();
|
|
2042
|
-
try {
|
|
2043
|
-
const stack = [projectRoot];
|
|
2044
|
-
while (stack.length > 0) {
|
|
2045
|
-
const dir = stack.pop();
|
|
2046
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2047
|
-
if (entry.isDirectory()) {
|
|
2048
|
-
if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
2049
|
-
} else if (entry.isFile()) {
|
|
2050
|
-
out.add(toRelPath(projectRoot, join(dir, entry.name)));
|
|
2051
|
-
if (out.size > MAX_FILES) return out;
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
} catch {
|
|
2056
|
-
return null;
|
|
2057
|
-
}
|
|
2058
|
-
return out;
|
|
2059
|
-
}
|
|
2060
1829
|
|
|
2061
1830
|
// src/lessons/keyword-match.ts
|
|
2062
1831
|
function deriveHaystackTokens(query) {
|
|
@@ -2143,6 +1912,29 @@ function triggerMatches(trigger, query, budget) {
|
|
|
2143
1912
|
}
|
|
2144
1913
|
}
|
|
2145
1914
|
|
|
1915
|
+
// src/lessons/ranking-signals.ts
|
|
1916
|
+
function buildFanout(graph) {
|
|
1917
|
+
const fanout = /* @__PURE__ */ new Map();
|
|
1918
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1919
|
+
if (lesson.status !== "active") continue;
|
|
1920
|
+
for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
|
|
1921
|
+
}
|
|
1922
|
+
return fanout;
|
|
1923
|
+
}
|
|
1924
|
+
function buildTopicCoherence(matches) {
|
|
1925
|
+
const topicCount = /* @__PURE__ */ new Map();
|
|
1926
|
+
for (const { lesson } of matches) {
|
|
1927
|
+
for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
|
|
1928
|
+
}
|
|
1929
|
+
const coherence = /* @__PURE__ */ new Map();
|
|
1930
|
+
for (const { id, lesson } of matches) {
|
|
1931
|
+
let best = 0;
|
|
1932
|
+
for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
|
|
1933
|
+
coherence.set(id, best);
|
|
1934
|
+
}
|
|
1935
|
+
return coherence;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
2146
1938
|
// src/lessons/ranking.ts
|
|
2147
1939
|
var DEFAULT_RECALL_LIMIT = 10;
|
|
2148
1940
|
var DEFAULT_RECALL_MAX_TOKENS = 400;
|
|
@@ -2236,7 +2028,8 @@ function defaultLessonsConfig() {
|
|
|
2236
2028
|
return {
|
|
2237
2029
|
recallLimit: DEFAULT_RECALL_LIMIT,
|
|
2238
2030
|
recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
|
|
2239
|
-
autoPrune: false
|
|
2031
|
+
autoPrune: false,
|
|
2032
|
+
repairTriggers: false
|
|
2240
2033
|
};
|
|
2241
2034
|
}
|
|
2242
2035
|
function positiveInt(value) {
|
|
@@ -2261,6 +2054,57 @@ function loadRecallConfig(projectRoot) {
|
|
|
2261
2054
|
return fallback;
|
|
2262
2055
|
}
|
|
2263
2056
|
}
|
|
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
|
|
2264
2108
|
function outcomeLogPath(projectRoot) {
|
|
2265
2109
|
return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
|
|
2266
2110
|
}
|
|
@@ -2377,14 +2221,17 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2377
2221
|
dedup,
|
|
2378
2222
|
lessons.map((l) => l.id)
|
|
2379
2223
|
);
|
|
2380
|
-
recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
|
|
2224
|
+
recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
|
|
2225
|
+
bypassed: false,
|
|
2226
|
+
session: options.sessionId
|
|
2227
|
+
});
|
|
2381
2228
|
return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
|
|
2382
2229
|
}
|
|
2383
2230
|
function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
|
|
2384
2231
|
if (!isTelemetryEnabled()) return;
|
|
2385
2232
|
const byKind = collectMatchedTriggersByKind(graph, query);
|
|
2386
2233
|
const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
|
|
2387
|
-
const session = sessionId();
|
|
2234
|
+
const session = options.session ?? sessionId();
|
|
2388
2235
|
appendRecallRecord(projectRoot, {
|
|
2389
2236
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2390
2237
|
hasFile: query.file !== void 0,
|
|
@@ -2404,6 +2251,255 @@ function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, opti
|
|
|
2404
2251
|
...session !== void 0 ? { session } : {}
|
|
2405
2252
|
});
|
|
2406
2253
|
}
|
|
2254
|
+
|
|
2255
|
+
// src/lessons/prune.ts
|
|
2256
|
+
function planPrune(graph, options = {}) {
|
|
2257
|
+
const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
|
|
2258
|
+
const fanout = buildFanout(graph);
|
|
2259
|
+
const trimmedLessons = [];
|
|
2260
|
+
const keptByLesson = /* @__PURE__ */ new Map();
|
|
2261
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2262
|
+
if (lesson.status !== "active") continue;
|
|
2263
|
+
if (options.trimOverCap === false || lesson.triggers.length <= cap) {
|
|
2264
|
+
keptByLesson.set(id, lesson.triggers);
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const ordered = [...lesson.triggers].sort((a, b) => {
|
|
2268
|
+
const fa = fanout.get(a);
|
|
2269
|
+
const fb = fanout.get(b);
|
|
2270
|
+
return fa !== fb ? fa - fb : a < b ? -1 : 1;
|
|
2271
|
+
});
|
|
2272
|
+
const drop = new Set(ordered.slice(cap));
|
|
2273
|
+
const kept = lesson.triggers.filter((t) => !drop.has(t));
|
|
2274
|
+
keptByLesson.set(id, kept);
|
|
2275
|
+
trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
|
|
2276
|
+
}
|
|
2277
|
+
const removedDeadGlobs = [];
|
|
2278
|
+
const unreachableLessons = [];
|
|
2279
|
+
if (options.knownPaths !== void 0) {
|
|
2280
|
+
const dead = deadFileGlobIds(graph, options.knownPaths);
|
|
2281
|
+
if (dead.size > 0) {
|
|
2282
|
+
for (const [id, kept] of keptByLesson) {
|
|
2283
|
+
const deadInLesson = kept.filter((t) => dead.has(t));
|
|
2284
|
+
if (deadInLesson.length === 0) continue;
|
|
2285
|
+
const remaining = kept.filter((t) => !dead.has(t));
|
|
2286
|
+
if (remaining.length >= 1) {
|
|
2287
|
+
keptByLesson.set(id, remaining);
|
|
2288
|
+
removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
|
|
2289
|
+
} else {
|
|
2290
|
+
unreachableLessons.push(id);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
unreachableLessons.sort();
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const live = /* @__PURE__ */ new Set();
|
|
2297
|
+
for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
|
|
2298
|
+
const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
|
|
2299
|
+
const referencedTopics = /* @__PURE__ */ new Set();
|
|
2300
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
2301
|
+
for (const topic of lesson.topics) referencedTopics.add(topic);
|
|
2302
|
+
}
|
|
2303
|
+
const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
|
|
2304
|
+
return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
|
|
2305
|
+
}
|
|
2306
|
+
function applyPruneToGraph(graph, plan) {
|
|
2307
|
+
for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
|
|
2308
|
+
const lesson = graph.lessons[trim.id];
|
|
2309
|
+
if (lesson === void 0) continue;
|
|
2310
|
+
const drop = new Set(trim.removedTriggers);
|
|
2311
|
+
graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
|
|
2312
|
+
}
|
|
2313
|
+
for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
|
|
2314
|
+
const dead = new Set(plan.removedTriggerIds);
|
|
2315
|
+
if (dead.size === 0) return;
|
|
2316
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2317
|
+
if (lesson.triggers.some((t) => dead.has(t))) {
|
|
2318
|
+
graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
for (const t of dead) delete graph.triggers[t];
|
|
2322
|
+
}
|
|
2323
|
+
function isEmptyPrunePlan(plan) {
|
|
2324
|
+
return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/lessons/auto-prune.ts
|
|
2328
|
+
function isAutoPruneEnabled(projectRoot) {
|
|
2329
|
+
const path = lessonsPaths(projectRoot).config;
|
|
2330
|
+
if (!existsSync(path)) return false;
|
|
2331
|
+
try {
|
|
2332
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2333
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
2334
|
+
return parsed.autoPrune === true;
|
|
2335
|
+
} catch {
|
|
2336
|
+
return false;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
async function maybeAutoPrune(projectRoot, knownPaths) {
|
|
2340
|
+
if (!isAutoPruneEnabled(projectRoot)) return null;
|
|
2341
|
+
const preview = tryLoadLessonsGraph(projectRoot);
|
|
2342
|
+
if (preview === null) return null;
|
|
2343
|
+
if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
|
|
2344
|
+
let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
|
|
2345
|
+
await mutateLessonsGraph(projectRoot, (graph) => {
|
|
2346
|
+
const plan = planPrune(graph, { trimOverCap: false, knownPaths });
|
|
2347
|
+
if (isEmptyPrunePlan(plan)) return;
|
|
2348
|
+
applyPruneToGraph(graph, plan);
|
|
2349
|
+
summary = {
|
|
2350
|
+
removedTriggers: plan.removedTriggerIds.length,
|
|
2351
|
+
removedTopics: plan.removedTopicIds.length,
|
|
2352
|
+
detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
|
|
2353
|
+
};
|
|
2354
|
+
});
|
|
2355
|
+
const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
|
|
2356
|
+
return total > 0 ? summary : null;
|
|
2357
|
+
}
|
|
2358
|
+
var MAX_CAPTURE_LOG_RECORDS = 5e3;
|
|
2359
|
+
var CAPTURE_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
2360
|
+
function captureLogPath(projectRoot) {
|
|
2361
|
+
return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
|
|
2362
|
+
}
|
|
2363
|
+
function appendCaptureRecord(projectRoot, record, env = process.env) {
|
|
2364
|
+
if (!isTelemetryEnabled(env)) return;
|
|
2365
|
+
appendJsonl(captureLogPath(projectRoot), record, {
|
|
2366
|
+
maxRecords: MAX_CAPTURE_LOG_RECORDS,
|
|
2367
|
+
trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
|
|
2371
|
+
if (!isTelemetryEnabled(env)) return;
|
|
2372
|
+
const session = sessionId(env);
|
|
2373
|
+
appendCaptureRecord(
|
|
2374
|
+
projectRoot,
|
|
2375
|
+
{
|
|
2376
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2377
|
+
isNewLesson: result?.isNewLesson ?? false,
|
|
2378
|
+
isNewTopic: result?.isNewTopic ?? false,
|
|
2379
|
+
newTriggerCount: result?.newTriggerIds.length ?? 0,
|
|
2380
|
+
triggerKinds,
|
|
2381
|
+
blocked: result === null,
|
|
2382
|
+
warningCodes: result?.warnings.map((w) => w.code) ?? [],
|
|
2383
|
+
...session !== void 0 ? { session } : {},
|
|
2384
|
+
...result !== null ? { lessonId: result.id } : {}
|
|
2385
|
+
},
|
|
2386
|
+
env
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2390
|
+
var MAX_FILES = 2e5;
|
|
2391
|
+
function listProjectFiles(projectRoot) {
|
|
2392
|
+
const out = /* @__PURE__ */ new Set();
|
|
2393
|
+
try {
|
|
2394
|
+
const stack = [projectRoot];
|
|
2395
|
+
while (stack.length > 0) {
|
|
2396
|
+
const dir = stack.pop();
|
|
2397
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2398
|
+
if (entry.isDirectory()) {
|
|
2399
|
+
if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
2400
|
+
} else if (entry.isFile()) {
|
|
2401
|
+
out.add(toRelPath(projectRoot, join(dir, entry.name)));
|
|
2402
|
+
if (out.size > MAX_FILES) return out;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
} catch {
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
return out;
|
|
2410
|
+
}
|
|
2411
|
+
function isTriggerRepairEnabled(projectRoot) {
|
|
2412
|
+
const path = lessonsPaths(projectRoot).config;
|
|
2413
|
+
if (!existsSync(path)) return false;
|
|
2414
|
+
try {
|
|
2415
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2416
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
2417
|
+
return parsed.repairTriggers === true;
|
|
2418
|
+
} catch {
|
|
2419
|
+
return false;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function evidencePath(evidence, knownPaths) {
|
|
2423
|
+
for (const entry of evidence ?? []) {
|
|
2424
|
+
const candidate = entry.replaceAll("\\", "/").replace(/(:\d+)+$/, "").trim();
|
|
2425
|
+
if (knownPaths.has(candidate)) return candidate;
|
|
2426
|
+
}
|
|
2427
|
+
return void 0;
|
|
2428
|
+
}
|
|
2429
|
+
function classGlobFor(path) {
|
|
2430
|
+
const slash = path.lastIndexOf("/");
|
|
2431
|
+
const dir = slash === -1 ? "" : path.slice(0, slash + 1);
|
|
2432
|
+
const base = path.slice(slash + 1);
|
|
2433
|
+
const dot = base.lastIndexOf(".");
|
|
2434
|
+
return `${dir}*${dot > 0 ? base.slice(dot) : ""}`;
|
|
2435
|
+
}
|
|
2436
|
+
function repairFileGlobs(files, evidence, knownPaths, repairs) {
|
|
2437
|
+
const out = [];
|
|
2438
|
+
for (const glob of files) {
|
|
2439
|
+
const needsNarrow = knownPaths !== void 0 && (isBroadGlob(glob) || fileGlobMatchCount(glob, knownPaths) > WIDE_GLOB_MATCH_COUNT);
|
|
2440
|
+
if (!needsNarrow || evidence === void 0 || !picomatch(glob, { dot: true })(evidence)) {
|
|
2441
|
+
if (!out.includes(glob)) out.push(glob);
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
const derived = classGlobFor(evidence);
|
|
2445
|
+
const derivedOk = derived !== glob && picomatch(derived, { dot: true })(evidence) && fileGlobMatchCount(derived, knownPaths) <= fileGlobMatchCount(glob, knownPaths);
|
|
2446
|
+
if (!derivedOk) {
|
|
2447
|
+
if (!out.includes(glob)) out.push(glob);
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if (!out.includes(derived)) out.push(derived);
|
|
2451
|
+
repairs.push({
|
|
2452
|
+
code: "NARROWED_GLOB",
|
|
2453
|
+
message: `Narrowed broad file glob "${glob}" to the evidence file's class "${derived}". Review: for general/library behavior, re-point at the file-CLASS recurrence surface (a '**/.../*Name*.ts'-style glob) where the rule will actually recur.`
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
return out;
|
|
2457
|
+
}
|
|
2458
|
+
function repairKeywords(keywords, repairs) {
|
|
2459
|
+
const out = [];
|
|
2460
|
+
for (const kw of keywords) {
|
|
2461
|
+
const tokens = tokenize(kw);
|
|
2462
|
+
if (tokens.length === 0) {
|
|
2463
|
+
repairs.push({
|
|
2464
|
+
code: "DROPPED_KEYWORD",
|
|
2465
|
+
message: `Dropped keyword trigger "${kw}" \u2014 it tokenizes to nothing (stopwords/short words only) and can never fire.`
|
|
2466
|
+
});
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
if (!out.includes(kw)) out.push(kw);
|
|
2470
|
+
if (!keywordNeedleLosesTokens(kw) && !isLowSignalKeyword(kw)) continue;
|
|
2471
|
+
const variant = tokens.slice(0, MAX_RECOMMENDED_KEYWORD_TOKENS).join(" ");
|
|
2472
|
+
if (variant.toLowerCase() === kw.toLowerCase() || out.includes(variant)) continue;
|
|
2473
|
+
out.push(variant);
|
|
2474
|
+
repairs.push({
|
|
2475
|
+
code: "KEYWORD_VARIANT_ADDED",
|
|
2476
|
+
message: `Keyword trigger "${kw}" cannot match on the mandatory --file/--cmd token path; added the matchable variant "${variant}" beside it (the original still matches prompt text).`
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
return out;
|
|
2480
|
+
}
|
|
2481
|
+
function repairTriggers(input, knownPaths) {
|
|
2482
|
+
const repairs = [];
|
|
2483
|
+
const evidence = knownPaths === void 0 ? void 0 : evidencePath(input.evidence, knownPaths);
|
|
2484
|
+
const files = input.triggers.files === void 0 ? void 0 : repairFileGlobs(input.triggers.files, evidence, knownPaths, repairs);
|
|
2485
|
+
const keywords = input.triggers.keywords === void 0 ? void 0 : repairKeywords(input.triggers.keywords, repairs);
|
|
2486
|
+
if (repairs.length === 0) return { input, repairs };
|
|
2487
|
+
const total = (files?.length ?? 0) + (input.triggers.commands?.length ?? 0) + (keywords?.length ?? 0);
|
|
2488
|
+
if (total === 0) return { input, repairs: [] };
|
|
2489
|
+
return {
|
|
2490
|
+
input: {
|
|
2491
|
+
...input,
|
|
2492
|
+
triggers: {
|
|
2493
|
+
...files !== void 0 ? { files } : {},
|
|
2494
|
+
...input.triggers.commands !== void 0 ? { commands: input.triggers.commands } : {},
|
|
2495
|
+
...keywords !== void 0 ? { keywords } : {}
|
|
2496
|
+
}
|
|
2497
|
+
},
|
|
2498
|
+
repairs
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// src/lessons/capture.ts
|
|
2407
2503
|
async function captureLesson(projectRoot, input, options = {}) {
|
|
2408
2504
|
await maybeAutoMigrateLessons(projectRoot);
|
|
2409
2505
|
const triggerKinds = {
|
|
@@ -2412,11 +2508,14 @@ async function captureLesson(projectRoot, input, options = {}) {
|
|
|
2412
2508
|
keyword: input.triggers.keywords?.length ?? 0
|
|
2413
2509
|
};
|
|
2414
2510
|
const knownPaths = options.knownPaths ?? listProjectFiles(projectRoot) ?? void 0;
|
|
2511
|
+
const repair = isTriggerRepairEnabled(projectRoot) ? repairTriggers(input, knownPaths) : null;
|
|
2512
|
+
const effective = repair === null ? input : repair.input;
|
|
2415
2513
|
try {
|
|
2416
|
-
const result = await addLesson(projectRoot,
|
|
2417
|
-
|
|
2514
|
+
const result = await addLesson(projectRoot, effective, { ...options, knownPaths });
|
|
2515
|
+
const repaired = repair === null || repair.repairs.length === 0 ? result : { ...result, warnings: [...result.warnings, ...repair.repairs] };
|
|
2516
|
+
recordCapture(projectRoot, triggerKinds, repaired);
|
|
2418
2517
|
const autoPruned = await maybeAutoPrune(projectRoot, knownPaths);
|
|
2419
|
-
return autoPruned === null ?
|
|
2518
|
+
return autoPruned === null ? repaired : { ...repaired, autoPruned };
|
|
2420
2519
|
} catch (err) {
|
|
2421
2520
|
recordCapture(projectRoot, triggerKinds, null);
|
|
2422
2521
|
throw err;
|