agentsmesh 0.31.0 → 0.33.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 +326 -0
- package/README.md +30 -29
- package/dist/canonical.d.ts +2 -2
- package/dist/canonical.js +16467 -9745
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +286 -228
- package/dist/engine.d.ts +17 -3
- package/dist/engine.js +12407 -5558
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +11848 -4982
- package/dist/index.js.map +1 -1
- package/dist/{init-PvpXanVd.d.ts → init-rvKjGLTB.d.ts} +75 -75
- package/dist/lessons.d.ts +16 -4
- package/dist/lessons.js +869 -611
- 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-DeS4XOtV.d.ts} +15 -2
- package/dist/targets.d.ts +3 -3
- package/dist/targets.js +16536 -9713
- package/dist/targets.js.map +1 -1
- package/package.json +2 -2
- package/schemas/agentsmesh.json +9 -0
- package/schemas/installs.json +3 -0
- package/schemas/pack.json +3 -0
package/dist/lessons.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, realpathSync, appendFileSync } from 'fs';
|
|
3
|
+
import { resolve, dirname, join, relative, sep, basename, extname } from 'path';
|
|
4
|
+
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
2
5
|
import { createHash } from 'crypto';
|
|
3
6
|
import picomatch from 'picomatch';
|
|
4
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync, statSync } from 'fs';
|
|
5
|
-
import { resolve, join, relative, sep, dirname, basename, extname } from 'path';
|
|
6
|
-
import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
|
|
7
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 {
|
|
@@ -856,7 +701,7 @@ function getHostname() {
|
|
|
856
701
|
return hostname();
|
|
857
702
|
}
|
|
858
703
|
function sleep(ms) {
|
|
859
|
-
return new Promise((
|
|
704
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
860
705
|
}
|
|
861
706
|
|
|
862
707
|
// src/lessons/lessons-lock.ts
|
|
@@ -1807,255 +1652,159 @@ function findExistingLessonByRule(graph, ruleKey) {
|
|
|
1807
1652
|
return null;
|
|
1808
1653
|
}
|
|
1809
1654
|
|
|
1810
|
-
// src/lessons/
|
|
1811
|
-
function
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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();
|
|
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);
|
|
1872
1675
|
}
|
|
1873
|
-
}
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
|
|
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
|
+
};
|
|
1883
1685
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
graph
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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];
|
|
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
|
+
};
|
|
1900
1697
|
}
|
|
1901
|
-
function
|
|
1902
|
-
return
|
|
1698
|
+
function toRelPath(projectRoot, absolute) {
|
|
1699
|
+
return relative(projectRoot, absolute).split(sep).join("/");
|
|
1903
1700
|
}
|
|
1701
|
+
var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
1904
1702
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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> --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
|
+
|
|
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";
|
|
1915
1717
|
}
|
|
1916
|
-
}
|
|
1917
|
-
async function
|
|
1918
|
-
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
+
});
|
|
1960
1764
|
}
|
|
1961
1765
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
}
|
|
1975
|
-
function isTelemetryEnabled(env = process.env) {
|
|
1976
|
-
return env[TELEMETRY_ENV] === "1";
|
|
1977
|
-
}
|
|
1978
|
-
function appendRecallRecord(projectRoot, record, env = process.env) {
|
|
1979
|
-
if (!isTelemetryEnabled(env)) return;
|
|
1980
|
-
appendJsonl(recallLogPath(projectRoot), record, {
|
|
1981
|
-
maxRecords: MAX_RECALL_LOG_RECORDS,
|
|
1982
|
-
trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
|
|
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
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
|
|
2000
|
-
if (!isTelemetryEnabled(env)) return;
|
|
2001
|
-
const session = sessionId(env);
|
|
2002
|
-
appendCaptureRecord(
|
|
2003
|
-
projectRoot,
|
|
2004
|
-
{
|
|
2005
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2006
|
-
isNewLesson: result?.isNewLesson ?? false,
|
|
2007
|
-
isNewTopic: result?.isNewTopic ?? false,
|
|
2008
|
-
newTriggerCount: result?.newTriggerIds.length ?? 0,
|
|
2009
|
-
triggerKinds,
|
|
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
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
function normalizeRecallFile(file, projectRoot) {
|
|
2019
|
-
const forward = file.replaceAll("\\", "/");
|
|
2020
|
-
const direct = relativize(projectRoot, forward);
|
|
2021
|
-
if (!direct.startsWith("../")) return direct;
|
|
2022
|
-
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
2023
|
-
return viaReal.startsWith("../") ? direct : viaReal;
|
|
2024
|
-
}
|
|
2025
|
-
function relativize(root, forward) {
|
|
2026
|
-
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
2027
|
-
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
2028
|
-
}
|
|
2029
|
-
function safeRealpath(path) {
|
|
2030
|
-
try {
|
|
2031
|
-
return realpathSync(path);
|
|
2032
|
-
} catch {
|
|
2033
|
-
const parent = dirname(path);
|
|
2034
|
-
if (parent === path) return path;
|
|
2035
|
-
return resolve(safeRealpath(parent), basename(path));
|
|
2036
|
-
}
|
|
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}`;
|
|
2037
1796
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
1797
|
+
async function maybeAutoMigrateLessons(projectRoot) {
|
|
1798
|
+
if (existsSync(graphFilePath(projectRoot))) return false;
|
|
1799
|
+
const paths = lessonsPaths(projectRoot);
|
|
1800
|
+
if (!existsSync(paths.index)) return false;
|
|
2042
1801
|
try {
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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;
|
|
1802
|
+
await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
|
|
1803
|
+
return true;
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
if (err instanceof LessonsGraphExistsError) return false;
|
|
1806
|
+
throw err;
|
|
2057
1807
|
}
|
|
2058
|
-
return out;
|
|
2059
1808
|
}
|
|
2060
1809
|
|
|
2061
1810
|
// src/lessons/keyword-match.ts
|
|
@@ -2142,6 +1891,171 @@ function triggerMatches(trigger, query, budget) {
|
|
|
2142
1891
|
return keywordMatches(trigger.pattern, query);
|
|
2143
1892
|
}
|
|
2144
1893
|
}
|
|
1894
|
+
function appendJsonl(path, record, opts) {
|
|
1895
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1896
|
+
appendFileSync(path, `${JSON.stringify(record)}
|
|
1897
|
+
`, "utf8");
|
|
1898
|
+
if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
|
|
1899
|
+
}
|
|
1900
|
+
function capJsonl(path, maxRecords) {
|
|
1901
|
+
if (!existsSync(path)) return;
|
|
1902
|
+
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
1903
|
+
if (lines.length <= maxRecords) return;
|
|
1904
|
+
const kept = lines.slice(lines.length - maxRecords);
|
|
1905
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
1906
|
+
writeFileSync(tmp, `${kept.join("\n")}
|
|
1907
|
+
`, "utf8");
|
|
1908
|
+
renameSync(tmp, path);
|
|
1909
|
+
}
|
|
1910
|
+
function readJsonl(path) {
|
|
1911
|
+
if (!existsSync(path)) return [];
|
|
1912
|
+
const out = [];
|
|
1913
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
1914
|
+
if (line.trim().length === 0) continue;
|
|
1915
|
+
try {
|
|
1916
|
+
out.push(JSON.parse(line));
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return out;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// src/lessons/telemetry.ts
|
|
1924
|
+
var MAX_RECALL_LOG_RECORDS = 5e3;
|
|
1925
|
+
var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
1926
|
+
var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
|
|
1927
|
+
var SESSION_ENV = "AGENTSMESH_SESSION_ID";
|
|
1928
|
+
function sessionId(env = process.env) {
|
|
1929
|
+
const raw = env[SESSION_ENV];
|
|
1930
|
+
return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
|
|
1931
|
+
}
|
|
1932
|
+
function recallLogPath(projectRoot) {
|
|
1933
|
+
return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
|
|
1934
|
+
}
|
|
1935
|
+
function isTelemetryEnabled(env = process.env) {
|
|
1936
|
+
return env[TELEMETRY_ENV] === "1";
|
|
1937
|
+
}
|
|
1938
|
+
function appendRecallRecord(projectRoot, record, env = process.env) {
|
|
1939
|
+
if (!isTelemetryEnabled(env)) return;
|
|
1940
|
+
appendJsonl(recallLogPath(projectRoot), record, {
|
|
1941
|
+
maxRecords: MAX_RECALL_LOG_RECORDS,
|
|
1942
|
+
trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/lessons/cmd-fastpath.ts
|
|
1947
|
+
var FASTPATH_DIR = "agentsmesh-lessons-cmdidx";
|
|
1948
|
+
function shortHash(value) {
|
|
1949
|
+
let h = 5381;
|
|
1950
|
+
for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
|
|
1951
|
+
return h.toString(36);
|
|
1952
|
+
}
|
|
1953
|
+
function commandFastpathCachePath(projectRoot) {
|
|
1954
|
+
return join(tmpdir(), FASTPATH_DIR, `${shortHash(resolve(projectRoot))}.json`);
|
|
1955
|
+
}
|
|
1956
|
+
function currentGraphStamp(projectRoot) {
|
|
1957
|
+
try {
|
|
1958
|
+
const s = statSync(graphFilePath(projectRoot));
|
|
1959
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
1960
|
+
} catch {
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function isStringArray(v) {
|
|
1965
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
1966
|
+
}
|
|
1967
|
+
function readCache(path) {
|
|
1968
|
+
try {
|
|
1969
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1970
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1971
|
+
const c = parsed;
|
|
1972
|
+
const stamp = c.stamp;
|
|
1973
|
+
if (typeof stamp?.mtimeMs !== "number" || typeof stamp.size !== "number" || !isStringArray(c.commandPatterns) || !isStringArray(c.keywordPatterns)) {
|
|
1974
|
+
return null;
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
stamp: { mtimeMs: stamp.mtimeMs, size: stamp.size },
|
|
1978
|
+
commandPatterns: c.commandPatterns,
|
|
1979
|
+
keywordPatterns: c.keywordPatterns
|
|
1980
|
+
};
|
|
1981
|
+
} catch {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function refreshCommandFastpath(projectRoot, graph, preReadStamp) {
|
|
1986
|
+
try {
|
|
1987
|
+
if (preReadStamp === null) return;
|
|
1988
|
+
const stamp = currentGraphStamp(projectRoot);
|
|
1989
|
+
if (stamp === null) return;
|
|
1990
|
+
if (stamp.mtimeMs !== preReadStamp.mtimeMs || stamp.size !== preReadStamp.size) return;
|
|
1991
|
+
const path = commandFastpathCachePath(projectRoot);
|
|
1992
|
+
const existing = readCache(path);
|
|
1993
|
+
if (existing !== null && existing.stamp.mtimeMs === stamp.mtimeMs && existing.stamp.size === stamp.size) {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1997
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1998
|
+
if (lesson.status !== "active" || lesson.scope === "always") continue;
|
|
1999
|
+
for (const t of lesson.triggers) reachable.add(t);
|
|
2000
|
+
}
|
|
2001
|
+
const commandPatterns = [];
|
|
2002
|
+
const keywordPatterns = [];
|
|
2003
|
+
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
2004
|
+
if (!reachable.has(id)) continue;
|
|
2005
|
+
if (trigger.kind === "command_pattern") commandPatterns.push(trigger.pattern);
|
|
2006
|
+
else if (trigger.kind === "keyword") keywordPatterns.push(trigger.pattern);
|
|
2007
|
+
}
|
|
2008
|
+
const cache2 = { stamp, commandPatterns, keywordPatterns };
|
|
2009
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2010
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2011
|
+
writeFileSync(tmp, JSON.stringify(cache2), "utf8");
|
|
2012
|
+
renameSync(tmp, path);
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
function normalizeRecallFile(file, projectRoot) {
|
|
2017
|
+
const forward = file.replaceAll("\\", "/");
|
|
2018
|
+
const direct = relativize(projectRoot, forward);
|
|
2019
|
+
if (!direct.startsWith("../")) return direct;
|
|
2020
|
+
const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
|
|
2021
|
+
return viaReal.startsWith("../") ? direct : viaReal;
|
|
2022
|
+
}
|
|
2023
|
+
function relativize(root, forward) {
|
|
2024
|
+
const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
|
|
2025
|
+
return rel === "" ? forward.replaceAll("\\", "/") : rel;
|
|
2026
|
+
}
|
|
2027
|
+
function safeRealpath(path) {
|
|
2028
|
+
try {
|
|
2029
|
+
return realpathSync(path);
|
|
2030
|
+
} catch {
|
|
2031
|
+
const parent = dirname(path);
|
|
2032
|
+
if (parent === path) return path;
|
|
2033
|
+
return resolve(safeRealpath(parent), basename(path));
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/lessons/ranking-signals.ts
|
|
2038
|
+
function buildFanout(graph) {
|
|
2039
|
+
const fanout = /* @__PURE__ */ new Map();
|
|
2040
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
2041
|
+
if (lesson.status !== "active") continue;
|
|
2042
|
+
for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
|
|
2043
|
+
}
|
|
2044
|
+
return fanout;
|
|
2045
|
+
}
|
|
2046
|
+
function buildTopicCoherence(matches) {
|
|
2047
|
+
const topicCount = /* @__PURE__ */ new Map();
|
|
2048
|
+
for (const { lesson } of matches) {
|
|
2049
|
+
for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
|
|
2050
|
+
}
|
|
2051
|
+
const coherence = /* @__PURE__ */ new Map();
|
|
2052
|
+
for (const { id, lesson } of matches) {
|
|
2053
|
+
let best = 0;
|
|
2054
|
+
for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
|
|
2055
|
+
coherence.set(id, best);
|
|
2056
|
+
}
|
|
2057
|
+
return coherence;
|
|
2058
|
+
}
|
|
2145
2059
|
|
|
2146
2060
|
// src/lessons/ranking.ts
|
|
2147
2061
|
var DEFAULT_RECALL_LIMIT = 10;
|
|
@@ -2236,7 +2150,8 @@ function defaultLessonsConfig() {
|
|
|
2236
2150
|
return {
|
|
2237
2151
|
recallLimit: DEFAULT_RECALL_LIMIT,
|
|
2238
2152
|
recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
|
|
2239
|
-
autoPrune: false
|
|
2153
|
+
autoPrune: false,
|
|
2154
|
+
repairTriggers: false
|
|
2240
2155
|
};
|
|
2241
2156
|
}
|
|
2242
2157
|
function positiveInt(value) {
|
|
@@ -2297,48 +2212,116 @@ function loadEffectiveness(projectRoot) {
|
|
|
2297
2212
|
return map;
|
|
2298
2213
|
}
|
|
2299
2214
|
var SEEN_DIR = "agentsmesh-lessons-seen";
|
|
2300
|
-
function
|
|
2301
|
-
if (options.disabled === true) return null;
|
|
2302
|
-
const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
|
|
2303
|
-
if (id === void 0) return null;
|
|
2304
|
-
const path = seenPath(id, options.projectRoot);
|
|
2305
|
-
return { sessionId: id, seen: loadSeen(path), path };
|
|
2306
|
-
}
|
|
2307
|
-
function shortHash(value) {
|
|
2215
|
+
function shortHash2(value) {
|
|
2308
2216
|
let h = 5381;
|
|
2309
2217
|
for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
|
|
2310
2218
|
return h.toString(36);
|
|
2311
2219
|
}
|
|
2312
|
-
function
|
|
2220
|
+
function seenStorePath(id, projectRoot) {
|
|
2313
2221
|
const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 200);
|
|
2314
|
-
const scoped = projectRoot === void 0 ? safe : `${safe}__${
|
|
2222
|
+
const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash2(resolve(projectRoot))}`;
|
|
2315
2223
|
return join(tmpdir(), SEEN_DIR, `${scoped}.json`);
|
|
2316
2224
|
}
|
|
2317
|
-
function
|
|
2318
|
-
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
2225
|
+
function readSeenStore(path) {
|
|
2226
|
+
if (!existsSync(path)) return { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2319
2227
|
try {
|
|
2320
2228
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2321
|
-
if (
|
|
2322
|
-
|
|
2229
|
+
if (Array.isArray(parsed)) {
|
|
2230
|
+
const ids = new Set(parsed.filter((x) => typeof x === "string"));
|
|
2231
|
+
return { ids, stamps: null };
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2234
|
+
const seen = parsed.seen;
|
|
2235
|
+
if (typeof seen === "object" && seen !== null) {
|
|
2236
|
+
const stamps = /* @__PURE__ */ new Map();
|
|
2237
|
+
for (const [id, ms] of Object.entries(seen)) {
|
|
2238
|
+
if (typeof ms === "number") stamps.set(id, ms);
|
|
2239
|
+
}
|
|
2240
|
+
const lastAt = parsed.lastAt;
|
|
2241
|
+
return {
|
|
2242
|
+
ids: new Set(stamps.keys()),
|
|
2243
|
+
stamps,
|
|
2244
|
+
...typeof lastAt === "number" ? { lastAt } : {}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2249
|
+
} catch {
|
|
2250
|
+
return { ids: /* @__PURE__ */ new Set(), stamps: null };
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function writeSeenStore(path, data, lastAt) {
|
|
2254
|
+
try {
|
|
2255
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2256
|
+
const body = data instanceof Map ? JSON.stringify({ v: 2, lastAt: lastAt ?? Date.now(), seen: Object.fromEntries(data) }) : JSON.stringify(data);
|
|
2257
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2258
|
+
writeFileSync(tmp, body, "utf8");
|
|
2259
|
+
renameSync(tmp, path);
|
|
2323
2260
|
} catch {
|
|
2324
|
-
return /* @__PURE__ */ new Set();
|
|
2325
2261
|
}
|
|
2326
2262
|
}
|
|
2263
|
+
var AUTO_SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
2264
|
+
var FUTURE_TOLERANCE_MS = 6e4;
|
|
2265
|
+
function stampAgeMs(stamp, now = Date.now()) {
|
|
2266
|
+
if (stamp - now > FUTURE_TOLERANCE_MS) return Number.POSITIVE_INFINITY;
|
|
2267
|
+
return Math.max(0, now - stamp);
|
|
2268
|
+
}
|
|
2269
|
+
function isIdleSession(stamps, lastAt) {
|
|
2270
|
+
if (stamps === null || stamps.size === 0) return false;
|
|
2271
|
+
let newest = lastAt ?? 0;
|
|
2272
|
+
for (const ms of stamps.values()) if (ms > newest) newest = ms;
|
|
2273
|
+
return stampAgeMs(newest) > AUTO_SESSION_IDLE_MS;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// src/lessons/seen-cache.ts
|
|
2277
|
+
function openSessionDedup(options = {}) {
|
|
2278
|
+
if (options.disabled === true) return null;
|
|
2279
|
+
const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
|
|
2280
|
+
if (id === void 0) return null;
|
|
2281
|
+
const path = seenStorePath(id, options.projectRoot);
|
|
2282
|
+
const store = readSeenStore(path);
|
|
2283
|
+
const stale = options.ttlMs !== void 0 && isIdleSession(store.stamps, store.lastAt);
|
|
2284
|
+
const stamps = stale ? null : store.stamps;
|
|
2285
|
+
return {
|
|
2286
|
+
sessionId: id,
|
|
2287
|
+
seen: stale ? /* @__PURE__ */ new Set() : visibleSeen(store.ids, stamps, options.ttlMs),
|
|
2288
|
+
path,
|
|
2289
|
+
stamps,
|
|
2290
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
function visibleSeen(ids, stamps, ttlMs) {
|
|
2294
|
+
if (ttlMs === void 0) return ids;
|
|
2295
|
+
if (stamps === null) return /* @__PURE__ */ new Set();
|
|
2296
|
+
const now = Date.now();
|
|
2297
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
2298
|
+
for (const [id, ms] of stamps) if (stampAgeMs(ms, now) <= ttlMs) fresh.add(id);
|
|
2299
|
+
return fresh;
|
|
2300
|
+
}
|
|
2327
2301
|
function filterUnseen(dedup, matches) {
|
|
2328
2302
|
return matches.filter((m) => !dedup.seen.has(m.id));
|
|
2329
2303
|
}
|
|
2330
2304
|
function commitSeen(dedup, returnedIds) {
|
|
2331
|
-
if (returnedIds.length === 0)
|
|
2305
|
+
if (returnedIds.length === 0) {
|
|
2306
|
+
if (dedup.ttlMs !== void 0 && dedup.stamps !== null) {
|
|
2307
|
+
writeSeenStore(dedup.path, dedup.stamps);
|
|
2308
|
+
}
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if (dedup.ttlMs !== void 0 || dedup.stamps !== null) {
|
|
2312
|
+
const now = Date.now();
|
|
2313
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2314
|
+
for (const [id, ms] of dedup.stamps ?? []) {
|
|
2315
|
+
if (dedup.ttlMs === void 0 || now - ms <= dedup.ttlMs) merged.set(id, ms);
|
|
2316
|
+
}
|
|
2317
|
+
for (const id of returnedIds) merged.set(id, now);
|
|
2318
|
+
writeSeenStore(dedup.path, merged);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2332
2321
|
const union3 = new Set(dedup.seen);
|
|
2333
2322
|
for (const id of returnedIds) union3.add(id);
|
|
2334
2323
|
if (union3.size === dedup.seen.size) return;
|
|
2335
|
-
|
|
2336
|
-
mkdirSync(dirname(dedup.path), { recursive: true });
|
|
2337
|
-
const tmp = `${dedup.path}.${process.pid}.tmp`;
|
|
2338
|
-
writeFileSync(tmp, JSON.stringify([...union3]), "utf8");
|
|
2339
|
-
renameSync(tmp, dedup.path);
|
|
2340
|
-
} catch {
|
|
2341
|
-
}
|
|
2324
|
+
writeSeenStore(dedup.path, [...union3]);
|
|
2342
2325
|
}
|
|
2343
2326
|
|
|
2344
2327
|
// src/lessons/recall.ts
|
|
@@ -2347,6 +2330,7 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2347
2330
|
await maybeAutoMigrateLessons(projectRoot);
|
|
2348
2331
|
} catch {
|
|
2349
2332
|
}
|
|
2333
|
+
const preReadStamp = currentGraphStamp(projectRoot);
|
|
2350
2334
|
const load = loadLessonsGraphResilient(projectRoot);
|
|
2351
2335
|
if (load.status === "corrupt") {
|
|
2352
2336
|
return { lessons: [], totalMatches: 0, suppressed: 0, corrupt: true };
|
|
@@ -2356,12 +2340,14 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2356
2340
|
}
|
|
2357
2341
|
if (load.status === "absent") return { lessons: [], totalMatches: 0, suppressed: 0 };
|
|
2358
2342
|
const graph = load.graph;
|
|
2343
|
+
refreshCommandFastpath(projectRoot, graph, preReadStamp);
|
|
2359
2344
|
const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
|
|
2360
2345
|
const matches = queryLessons(graph, matchQuery);
|
|
2361
2346
|
const dedup = openSessionDedup({
|
|
2362
2347
|
explicit: options.sessionId,
|
|
2363
2348
|
disabled: options.noDedup,
|
|
2364
|
-
projectRoot
|
|
2349
|
+
projectRoot,
|
|
2350
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
|
|
2365
2351
|
});
|
|
2366
2352
|
const forRank = dedup === null ? matches : filterUnseen(dedup, matches);
|
|
2367
2353
|
const cfg = loadRecallConfig(projectRoot);
|
|
@@ -2369,22 +2355,27 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2369
2355
|
limit: options.limit ?? cfg.limit,
|
|
2370
2356
|
maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens,
|
|
2371
2357
|
// Down-rank proven fire-but-fail lessons (empty ⇒ neutral, so recall is
|
|
2372
|
-
// unchanged until the outcome log has real signal). Read from the side-channel
|
|
2373
|
-
|
|
2358
|
+
// unchanged until the outcome log has real signal). Read from the side-channel
|
|
2359
|
+
// only when something survived matching+dedup — a no-match recall must not pay
|
|
2360
|
+
// the (up to 2MB) outcome-log read for a ranking of nothing.
|
|
2361
|
+
effectiveness: forRank.length === 0 ? /* @__PURE__ */ new Map() : loadEffectiveness(projectRoot)
|
|
2374
2362
|
});
|
|
2375
2363
|
if (dedup !== null)
|
|
2376
2364
|
commitSeen(
|
|
2377
2365
|
dedup,
|
|
2378
2366
|
lessons.map((l) => l.id)
|
|
2379
2367
|
);
|
|
2380
|
-
recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
|
|
2368
|
+
recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
|
|
2369
|
+
bypassed: false,
|
|
2370
|
+
session: options.sessionId
|
|
2371
|
+
});
|
|
2381
2372
|
return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
|
|
2382
2373
|
}
|
|
2383
2374
|
function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
|
|
2384
2375
|
if (!isTelemetryEnabled()) return;
|
|
2385
2376
|
const byKind = collectMatchedTriggersByKind(graph, query);
|
|
2386
2377
|
const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
|
|
2387
|
-
const session = sessionId();
|
|
2378
|
+
const session = options.session ?? sessionId();
|
|
2388
2379
|
appendRecallRecord(projectRoot, {
|
|
2389
2380
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2390
2381
|
hasFile: query.file !== void 0,
|
|
@@ -2404,6 +2395,255 @@ function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, opti
|
|
|
2404
2395
|
...session !== void 0 ? { session } : {}
|
|
2405
2396
|
});
|
|
2406
2397
|
}
|
|
2398
|
+
|
|
2399
|
+
// src/lessons/prune.ts
|
|
2400
|
+
function planPrune(graph, options = {}) {
|
|
2401
|
+
const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
|
|
2402
|
+
const fanout = buildFanout(graph);
|
|
2403
|
+
const trimmedLessons = [];
|
|
2404
|
+
const keptByLesson = /* @__PURE__ */ new Map();
|
|
2405
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2406
|
+
if (lesson.status !== "active") continue;
|
|
2407
|
+
if (options.trimOverCap === false || lesson.triggers.length <= cap) {
|
|
2408
|
+
keptByLesson.set(id, lesson.triggers);
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
const ordered = [...lesson.triggers].sort((a, b) => {
|
|
2412
|
+
const fa = fanout.get(a);
|
|
2413
|
+
const fb = fanout.get(b);
|
|
2414
|
+
return fa !== fb ? fa - fb : a < b ? -1 : 1;
|
|
2415
|
+
});
|
|
2416
|
+
const drop = new Set(ordered.slice(cap));
|
|
2417
|
+
const kept = lesson.triggers.filter((t) => !drop.has(t));
|
|
2418
|
+
keptByLesson.set(id, kept);
|
|
2419
|
+
trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
|
|
2420
|
+
}
|
|
2421
|
+
const removedDeadGlobs = [];
|
|
2422
|
+
const unreachableLessons = [];
|
|
2423
|
+
if (options.knownPaths !== void 0) {
|
|
2424
|
+
const dead = deadFileGlobIds(graph, options.knownPaths);
|
|
2425
|
+
if (dead.size > 0) {
|
|
2426
|
+
for (const [id, kept] of keptByLesson) {
|
|
2427
|
+
const deadInLesson = kept.filter((t) => dead.has(t));
|
|
2428
|
+
if (deadInLesson.length === 0) continue;
|
|
2429
|
+
const remaining = kept.filter((t) => !dead.has(t));
|
|
2430
|
+
if (remaining.length >= 1) {
|
|
2431
|
+
keptByLesson.set(id, remaining);
|
|
2432
|
+
removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
|
|
2433
|
+
} else {
|
|
2434
|
+
unreachableLessons.push(id);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
unreachableLessons.sort();
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
const live = /* @__PURE__ */ new Set();
|
|
2441
|
+
for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
|
|
2442
|
+
const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
|
|
2443
|
+
const referencedTopics = /* @__PURE__ */ new Set();
|
|
2444
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
2445
|
+
for (const topic of lesson.topics) referencedTopics.add(topic);
|
|
2446
|
+
}
|
|
2447
|
+
const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
|
|
2448
|
+
return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
|
|
2449
|
+
}
|
|
2450
|
+
function applyPruneToGraph(graph, plan) {
|
|
2451
|
+
for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
|
|
2452
|
+
const lesson = graph.lessons[trim.id];
|
|
2453
|
+
if (lesson === void 0) continue;
|
|
2454
|
+
const drop = new Set(trim.removedTriggers);
|
|
2455
|
+
graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
|
|
2456
|
+
}
|
|
2457
|
+
for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
|
|
2458
|
+
const dead = new Set(plan.removedTriggerIds);
|
|
2459
|
+
if (dead.size === 0) return;
|
|
2460
|
+
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2461
|
+
if (lesson.triggers.some((t) => dead.has(t))) {
|
|
2462
|
+
graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
for (const t of dead) delete graph.triggers[t];
|
|
2466
|
+
}
|
|
2467
|
+
function isEmptyPrunePlan(plan) {
|
|
2468
|
+
return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/lessons/auto-prune.ts
|
|
2472
|
+
function isAutoPruneEnabled(projectRoot) {
|
|
2473
|
+
const path = lessonsPaths(projectRoot).config;
|
|
2474
|
+
if (!existsSync(path)) return false;
|
|
2475
|
+
try {
|
|
2476
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2477
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
2478
|
+
return parsed.autoPrune === true;
|
|
2479
|
+
} catch {
|
|
2480
|
+
return false;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
async function maybeAutoPrune(projectRoot, knownPaths) {
|
|
2484
|
+
if (!isAutoPruneEnabled(projectRoot)) return null;
|
|
2485
|
+
const preview = tryLoadLessonsGraph(projectRoot);
|
|
2486
|
+
if (preview === null) return null;
|
|
2487
|
+
if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
|
|
2488
|
+
let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
|
|
2489
|
+
await mutateLessonsGraph(projectRoot, (graph) => {
|
|
2490
|
+
const plan = planPrune(graph, { trimOverCap: false, knownPaths });
|
|
2491
|
+
if (isEmptyPrunePlan(plan)) return;
|
|
2492
|
+
applyPruneToGraph(graph, plan);
|
|
2493
|
+
summary = {
|
|
2494
|
+
removedTriggers: plan.removedTriggerIds.length,
|
|
2495
|
+
removedTopics: plan.removedTopicIds.length,
|
|
2496
|
+
detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
|
|
2497
|
+
};
|
|
2498
|
+
});
|
|
2499
|
+
const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
|
|
2500
|
+
return total > 0 ? summary : null;
|
|
2501
|
+
}
|
|
2502
|
+
var MAX_CAPTURE_LOG_RECORDS = 5e3;
|
|
2503
|
+
var CAPTURE_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
2504
|
+
function captureLogPath(projectRoot) {
|
|
2505
|
+
return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
|
|
2506
|
+
}
|
|
2507
|
+
function appendCaptureRecord(projectRoot, record, env = process.env) {
|
|
2508
|
+
if (!isTelemetryEnabled(env)) return;
|
|
2509
|
+
appendJsonl(captureLogPath(projectRoot), record, {
|
|
2510
|
+
maxRecords: MAX_CAPTURE_LOG_RECORDS,
|
|
2511
|
+
trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
|
|
2515
|
+
if (!isTelemetryEnabled(env)) return;
|
|
2516
|
+
const session = sessionId(env);
|
|
2517
|
+
appendCaptureRecord(
|
|
2518
|
+
projectRoot,
|
|
2519
|
+
{
|
|
2520
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2521
|
+
isNewLesson: result?.isNewLesson ?? false,
|
|
2522
|
+
isNewTopic: result?.isNewTopic ?? false,
|
|
2523
|
+
newTriggerCount: result?.newTriggerIds.length ?? 0,
|
|
2524
|
+
triggerKinds,
|
|
2525
|
+
blocked: result === null,
|
|
2526
|
+
warningCodes: result?.warnings.map((w) => w.code) ?? [],
|
|
2527
|
+
...session !== void 0 ? { session } : {},
|
|
2528
|
+
...result !== null ? { lessonId: result.id } : {}
|
|
2529
|
+
},
|
|
2530
|
+
env
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
2534
|
+
var MAX_FILES = 2e5;
|
|
2535
|
+
function listProjectFiles(projectRoot) {
|
|
2536
|
+
const out = /* @__PURE__ */ new Set();
|
|
2537
|
+
try {
|
|
2538
|
+
const stack = [projectRoot];
|
|
2539
|
+
while (stack.length > 0) {
|
|
2540
|
+
const dir = stack.pop();
|
|
2541
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2542
|
+
if (entry.isDirectory()) {
|
|
2543
|
+
if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
2544
|
+
} else if (entry.isFile()) {
|
|
2545
|
+
out.add(toRelPath(projectRoot, join(dir, entry.name)));
|
|
2546
|
+
if (out.size > MAX_FILES) return out;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
} catch {
|
|
2551
|
+
return null;
|
|
2552
|
+
}
|
|
2553
|
+
return out;
|
|
2554
|
+
}
|
|
2555
|
+
function isTriggerRepairEnabled(projectRoot) {
|
|
2556
|
+
const path = lessonsPaths(projectRoot).config;
|
|
2557
|
+
if (!existsSync(path)) return false;
|
|
2558
|
+
try {
|
|
2559
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2560
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
2561
|
+
return parsed.repairTriggers === true;
|
|
2562
|
+
} catch {
|
|
2563
|
+
return false;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function evidencePath(evidence, knownPaths) {
|
|
2567
|
+
for (const entry of evidence ?? []) {
|
|
2568
|
+
const candidate = entry.replaceAll("\\", "/").replace(/(:\d+)+$/, "").trim();
|
|
2569
|
+
if (knownPaths.has(candidate)) return candidate;
|
|
2570
|
+
}
|
|
2571
|
+
return void 0;
|
|
2572
|
+
}
|
|
2573
|
+
function classGlobFor(path) {
|
|
2574
|
+
const slash = path.lastIndexOf("/");
|
|
2575
|
+
const dir = slash === -1 ? "" : path.slice(0, slash + 1);
|
|
2576
|
+
const base = path.slice(slash + 1);
|
|
2577
|
+
const dot = base.lastIndexOf(".");
|
|
2578
|
+
return `${dir}*${dot > 0 ? base.slice(dot) : ""}`;
|
|
2579
|
+
}
|
|
2580
|
+
function repairFileGlobs(files, evidence, knownPaths, repairs) {
|
|
2581
|
+
const out = [];
|
|
2582
|
+
for (const glob of files) {
|
|
2583
|
+
const needsNarrow = knownPaths !== void 0 && (isBroadGlob(glob) || fileGlobMatchCount(glob, knownPaths) > WIDE_GLOB_MATCH_COUNT);
|
|
2584
|
+
if (!needsNarrow || evidence === void 0 || !picomatch(glob, { dot: true })(evidence)) {
|
|
2585
|
+
if (!out.includes(glob)) out.push(glob);
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
const derived = classGlobFor(evidence);
|
|
2589
|
+
const derivedOk = derived !== glob && picomatch(derived, { dot: true })(evidence) && fileGlobMatchCount(derived, knownPaths) <= fileGlobMatchCount(glob, knownPaths);
|
|
2590
|
+
if (!derivedOk) {
|
|
2591
|
+
if (!out.includes(glob)) out.push(glob);
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
if (!out.includes(derived)) out.push(derived);
|
|
2595
|
+
repairs.push({
|
|
2596
|
+
code: "NARROWED_GLOB",
|
|
2597
|
+
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.`
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
return out;
|
|
2601
|
+
}
|
|
2602
|
+
function repairKeywords(keywords, repairs) {
|
|
2603
|
+
const out = [];
|
|
2604
|
+
for (const kw of keywords) {
|
|
2605
|
+
const tokens = tokenize(kw);
|
|
2606
|
+
if (tokens.length === 0) {
|
|
2607
|
+
repairs.push({
|
|
2608
|
+
code: "DROPPED_KEYWORD",
|
|
2609
|
+
message: `Dropped keyword trigger "${kw}" \u2014 it tokenizes to nothing (stopwords/short words only) and can never fire.`
|
|
2610
|
+
});
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
if (!out.includes(kw)) out.push(kw);
|
|
2614
|
+
if (!keywordNeedleLosesTokens(kw) && !isLowSignalKeyword(kw)) continue;
|
|
2615
|
+
const variant = tokens.slice(0, MAX_RECOMMENDED_KEYWORD_TOKENS).join(" ");
|
|
2616
|
+
if (variant.toLowerCase() === kw.toLowerCase() || out.includes(variant)) continue;
|
|
2617
|
+
out.push(variant);
|
|
2618
|
+
repairs.push({
|
|
2619
|
+
code: "KEYWORD_VARIANT_ADDED",
|
|
2620
|
+
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).`
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
return out;
|
|
2624
|
+
}
|
|
2625
|
+
function repairTriggers(input, knownPaths) {
|
|
2626
|
+
const repairs = [];
|
|
2627
|
+
const evidence = knownPaths === void 0 ? void 0 : evidencePath(input.evidence, knownPaths);
|
|
2628
|
+
const files = input.triggers.files === void 0 ? void 0 : repairFileGlobs(input.triggers.files, evidence, knownPaths, repairs);
|
|
2629
|
+
const keywords = input.triggers.keywords === void 0 ? void 0 : repairKeywords(input.triggers.keywords, repairs);
|
|
2630
|
+
if (repairs.length === 0) return { input, repairs };
|
|
2631
|
+
const total = (files?.length ?? 0) + (input.triggers.commands?.length ?? 0) + (keywords?.length ?? 0);
|
|
2632
|
+
if (total === 0) return { input, repairs: [] };
|
|
2633
|
+
return {
|
|
2634
|
+
input: {
|
|
2635
|
+
...input,
|
|
2636
|
+
triggers: {
|
|
2637
|
+
...files !== void 0 ? { files } : {},
|
|
2638
|
+
...input.triggers.commands !== void 0 ? { commands: input.triggers.commands } : {},
|
|
2639
|
+
...keywords !== void 0 ? { keywords } : {}
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
repairs
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/lessons/capture.ts
|
|
2407
2647
|
async function captureLesson(projectRoot, input, options = {}) {
|
|
2408
2648
|
await maybeAutoMigrateLessons(projectRoot);
|
|
2409
2649
|
const triggerKinds = {
|
|
@@ -2412,11 +2652,14 @@ async function captureLesson(projectRoot, input, options = {}) {
|
|
|
2412
2652
|
keyword: input.triggers.keywords?.length ?? 0
|
|
2413
2653
|
};
|
|
2414
2654
|
const knownPaths = options.knownPaths ?? listProjectFiles(projectRoot) ?? void 0;
|
|
2655
|
+
const repair = isTriggerRepairEnabled(projectRoot) ? repairTriggers(input, knownPaths) : null;
|
|
2656
|
+
const effective = repair === null ? input : repair.input;
|
|
2415
2657
|
try {
|
|
2416
|
-
const result = await addLesson(projectRoot,
|
|
2417
|
-
|
|
2658
|
+
const result = await addLesson(projectRoot, effective, { ...options, knownPaths });
|
|
2659
|
+
const repaired = repair === null || repair.repairs.length === 0 ? result : { ...result, warnings: [...result.warnings, ...repair.repairs] };
|
|
2660
|
+
recordCapture(projectRoot, triggerKinds, repaired);
|
|
2418
2661
|
const autoPruned = await maybeAutoPrune(projectRoot, knownPaths);
|
|
2419
|
-
return autoPruned === null ?
|
|
2662
|
+
return autoPruned === null ? repaired : { ...repaired, autoPruned };
|
|
2420
2663
|
} catch (err) {
|
|
2421
2664
|
recordCapture(projectRoot, triggerKinds, null);
|
|
2422
2665
|
throw err;
|
|
@@ -2736,6 +2979,17 @@ ${placed}` : placed;
|
|
|
2736
2979
|
|
|
2737
2980
|
// src/targets/projection/lessons-paragraph.ts
|
|
2738
2981
|
var LEGACY_RAW_FORMS = [
|
|
2982
|
+
// Pre-`--session auto` wording (2026-07): strip sentinel-less copies so a
|
|
2983
|
+
// project scaffolded before the dedup correlator dedups on the next scaffold.
|
|
2984
|
+
`## Lessons (BLOCKING)
|
|
2985
|
+
|
|
2986
|
+
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
2987
|
+
|
|
2988
|
+
**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.
|
|
2989
|
+
|
|
2990
|
+
**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>\`.
|
|
2991
|
+
|
|
2992
|
+
**Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`,
|
|
2739
2993
|
`## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
|
|
2740
2994
|
|
|
2741
2995
|
Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
|
|
@@ -2793,14 +3047,18 @@ regression / wrong assumption / surprise and you have not captured (nor stated
|
|
|
2793
3047
|
|
|
2794
3048
|
## Recall \u2014 before each edit/command, and at task start
|
|
2795
3049
|
|
|
2796
|
-
\`agentsmesh lessons query --file <path> --cmd <command
|
|
2797
|
-
Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
|
|
3050
|
+
\`agentsmesh lessons query --file <path> --cmd <command> --session auto\`, then apply every
|
|
3051
|
+
rule. Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
|
|
2798
3052
|
for a specific edit is the anti-pattern** \u2014 anchor those to \`--file\`/\`--cmd\`. But at the
|
|
2799
3053
|
START of a task (or when planning), run \`agentsmesh lessons query --keyword "<the task's
|
|
2800
|
-
key terms>" --always\`: that surfaces the conceptual rules no file/command
|
|
2801
|
-
universal always-on lessons \u2014 the manual equivalent of the automatic prompt
|
|
2802
|
-
hook-capable tools.
|
|
2803
|
-
so
|
|
3054
|
+
key terms>" --always --session auto\`: that surfaces the conceptual rules no file/command
|
|
3055
|
+
names PLUS the universal always-on lessons \u2014 the manual equivalent of the automatic prompt
|
|
3056
|
+
recall on hook-capable tools. **Always pass \`--session auto\`**: it suppresses rules already
|
|
3057
|
+
shown this session so repeat recalls stay quiet (without it every recall re-delivers the
|
|
3058
|
+
whole matched set); \`--no-dedup\` re-shows everything after a context reset. Author a
|
|
3059
|
+
\`keyword\` trigger beside a \`file_glob\` on conceptual lessons so they are reachable both
|
|
3060
|
+
ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`; session dedup
|
|
3061
|
+
is on by default there \u2014 \`no_dedup:true\` to re-show).
|
|
2804
3062
|
|
|
2805
3063
|
## Capture \u2014 Gate Function (before any completion claim)
|
|
2806
3064
|
|