agentsmesh 0.28.0 → 0.30.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 +42 -0
- package/README.md +64 -11
- package/dist/canonical.d.ts +2 -2
- package/dist/canonical.js +91 -13
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +223 -199
- package/dist/engine.d.ts +2 -2
- package/dist/engine.js +113 -17
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +235 -49
- package/dist/index.js.map +1 -1
- package/dist/{init-B0aI-g8W.d.ts → init-PvpXanVd.d.ts} +17 -3
- package/dist/lessons.d.ts +2 -2
- package/dist/lessons.js +195 -40
- package/dist/lessons.js.map +1 -1
- package/dist/{schema-CzaoYJlG.d.ts → schema-CH_JJep8.d.ts} +5 -0
- package/dist/{target-descriptor-ItDY3NR0.d.ts → target-descriptor-CaLUz7SR.d.ts} +1 -1
- package/dist/targets.d.ts +3 -3
- package/dist/targets.js +28 -9
- package/dist/targets.js.map +1 -1
- package/package.json +1 -1
- package/schemas/agentsmesh.json +11 -0
|
@@ -34,9 +34,10 @@ declare const LessonSchema: z.ZodObject<{
|
|
|
34
34
|
}>;
|
|
35
35
|
supersededBy: z.ZodOptional<z.ZodString>;
|
|
36
36
|
createdAt: z.ZodString;
|
|
37
|
+
scope: z.ZodOptional<z.ZodLiteral<"always">>;
|
|
37
38
|
}, z.core.$strict>;
|
|
38
39
|
declare const LessonsGraphSchema: z.ZodObject<{
|
|
39
|
-
version: z.ZodLiteral<1>;
|
|
40
|
+
version: z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>;
|
|
40
41
|
lessons: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
41
42
|
rule: z.ZodString;
|
|
42
43
|
rationale: z.ZodOptional<z.ZodString>;
|
|
@@ -50,6 +51,7 @@ declare const LessonsGraphSchema: z.ZodObject<{
|
|
|
50
51
|
}>;
|
|
51
52
|
supersededBy: z.ZodOptional<z.ZodString>;
|
|
52
53
|
createdAt: z.ZodString;
|
|
54
|
+
scope: z.ZodOptional<z.ZodLiteral<"always">>;
|
|
53
55
|
}, z.core.$strict>>;
|
|
54
56
|
topics: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
55
57
|
summary: z.ZodString;
|
|
@@ -91,7 +93,7 @@ interface AutoPruneSummary {
|
|
|
91
93
|
* because that lesson is captured then silently never recalled. These guardrails
|
|
92
94
|
* are the warn-only complement to that single hard block.
|
|
93
95
|
*/
|
|
94
|
-
type GuardrailCode = 'OVERSIZED_LESSON_TRIGGERS' | 'BROAD_GLOB_TRIGGER' | 'KEYWORD_ONLY_LESSON' | 'LOW_SIGNAL_KEYWORD' | 'STOPWORD_KEYWORD' | 'DEAD_GLOB' | 'NEAR_DUPLICATE_LESSON';
|
|
96
|
+
type GuardrailCode = 'OVERSIZED_LESSON_TRIGGERS' | 'BROAD_GLOB_TRIGGER' | 'WIDE_GLOB_MATCH' | 'KEYWORD_ONLY_LESSON' | 'LOW_SIGNAL_KEYWORD' | 'STOPWORD_KEYWORD' | 'DEAD_GLOB' | 'NEAR_DUPLICATE_LESSON';
|
|
95
97
|
interface GuardrailWarning {
|
|
96
98
|
readonly code: GuardrailCode;
|
|
97
99
|
readonly message: string;
|
|
@@ -120,6 +122,8 @@ interface AddLessonInput {
|
|
|
120
122
|
readonly evidence?: readonly string[];
|
|
121
123
|
readonly rationale?: string;
|
|
122
124
|
readonly createdAt?: string;
|
|
125
|
+
/** `'always'` = a universal always-on lesson (no trigger needed; gates skipped). */
|
|
126
|
+
readonly scope?: 'always';
|
|
123
127
|
}
|
|
124
128
|
interface AddLessonOptions {
|
|
125
129
|
readonly allowNewTopic?: boolean;
|
|
@@ -197,6 +201,14 @@ interface RankOptions {
|
|
|
197
201
|
* worse for the agent than one slightly-over-budget rule.
|
|
198
202
|
*/
|
|
199
203
|
readonly maxTokens?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Per-lesson effectiveness score in [0,1] from the outcome log (1 = always
|
|
206
|
+
* helped; absent = neutral). Fed as a LOW-weight RRF signal so a proven
|
|
207
|
+
* fire-but-fail lesson sinks below an equally-matched effective one — a
|
|
208
|
+
* corrective nudge, never a driver. Empty/absent ⇒ every lesson ties on this
|
|
209
|
+
* signal ⇒ ordering is unchanged from the pre-effectiveness ranker.
|
|
210
|
+
*/
|
|
211
|
+
readonly effectiveness?: ReadonlyMap<string, number>;
|
|
200
212
|
}
|
|
201
213
|
/** Default recall cap: a broad trigger match returns the most-relevant few, not the whole topic. */
|
|
202
214
|
declare const DEFAULT_RECALL_LIMIT = 10;
|
|
@@ -457,7 +469,7 @@ declare function toRelPath(projectRoot: string, absolute: string): string;
|
|
|
457
469
|
* survives generate → import → generate round-trip; only the wording inside each
|
|
458
470
|
* block is tightened for maximum agent compliance.
|
|
459
471
|
*/
|
|
460
|
-
declare const LESSONS_PROCEDURAL_RULE = "## Lessons (BLOCKING)\n\nGraph `.agentsmesh/lessons/lessons.json` is canonical; never hand-edit it. Manual: `lessons` skill.\n\n**Recall:** before every file edit or state-changing command, MUST run `agentsmesh lessons query --file <path> --cmd <command>` and obey matches. Pure-read commands and recall itself are exempt.\n\n**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>`.\n\n**Before final:** report `Lesson: captured <id>` or `Lesson: none`. No recall/capture gate = task incomplete. No shell: use `lessons_query` / `lessons_add`.";
|
|
472
|
+
declare const LESSONS_PROCEDURAL_RULE = "## Lessons (BLOCKING)\n\nGraph `.agentsmesh/lessons/lessons.json` is canonical; never hand-edit it. Manual: `lessons` skill.\n\n**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.\n\n**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>`.\n\n**Before final:** report `Lesson: captured <id>` or `Lesson: none`. No recall/capture gate = task incomplete. No shell: use `lessons_query` / `lessons_add`.";
|
|
461
473
|
|
|
462
474
|
interface ScaffoldLessonsResult {
|
|
463
475
|
readonly created: string[];
|
|
@@ -467,6 +479,8 @@ interface ScaffoldLessonsResult {
|
|
|
467
479
|
readonly rootRuleUpdated: boolean;
|
|
468
480
|
/** True when the recall-log gitignore entry was added to `.gitignore`. */
|
|
469
481
|
readonly gitignoreUpdated: boolean;
|
|
482
|
+
/** True when the lessons.json merge-driver entry was added to `.gitattributes`. */
|
|
483
|
+
readonly gitattributesUpdated: boolean;
|
|
470
484
|
/** True when the PostToolUse recall hook was injected into `hooks.yaml`. */
|
|
471
485
|
readonly recallHookInjected: boolean;
|
|
472
486
|
}
|
package/dist/lessons.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as RankedLesson, A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult, j as LessonsQuery } from './init-
|
|
2
|
-
export { c as AddLessonTriggers, D as DEFAULT_RECALL_LIMIT, X as DEFAULT_RECALL_MAX_TOKENS, I as ImportLegacyOptions, d as ImportLegacyReport, Y as LESSONS_LOCK_FILENAME, L as LESSONS_PROCEDURAL_RULE, e as Lesson, f as LessonStatus, g as LessonsGraph, Z as LessonsGraphExistsError, h as LessonsGraphSchema, i as LessonsPaths, M as MatchedLesson, k as MergeLessonsOptions, l as MergeLessonsResult, m as MutateOptions, R as RankOptions, n as RankReason, S as ScaffoldLessonsResult, p as StripMarkersOptions, q as StripMarkersReport, T as Topic, r as Trigger, s as TriggerKind, U as UnknownTopicError, V as ValidationFinding, t as ValidationLevel, u as ValidationReport, v as acquireLessonsLock, w as addLesson, x as graphFilePath, y as importLegacyLessons, _ as lessonsLockPath, z as lessonsPaths, B as loadLessonsGraph, C as mergeLessons, E as mutateLessonsGraph, F as parseGraph, G as queryLessons, H as rankLessons, J as scaffoldLessons, K as serializeGraph, N as stripLegacyMarkers, O as stripMarkersInGraph, P as toRelPath, Q as tryLoadLessonsGraph, W as validateLessonsGraph } from './init-
|
|
1
|
+
import { o as RankedLesson, A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult, j as LessonsQuery } from './init-PvpXanVd.js';
|
|
2
|
+
export { c as AddLessonTriggers, D as DEFAULT_RECALL_LIMIT, X as DEFAULT_RECALL_MAX_TOKENS, I as ImportLegacyOptions, d as ImportLegacyReport, Y as LESSONS_LOCK_FILENAME, L as LESSONS_PROCEDURAL_RULE, e as Lesson, f as LessonStatus, g as LessonsGraph, Z as LessonsGraphExistsError, h as LessonsGraphSchema, i as LessonsPaths, M as MatchedLesson, k as MergeLessonsOptions, l as MergeLessonsResult, m as MutateOptions, R as RankOptions, n as RankReason, S as ScaffoldLessonsResult, p as StripMarkersOptions, q as StripMarkersReport, T as Topic, r as Trigger, s as TriggerKind, U as UnknownTopicError, V as ValidationFinding, t as ValidationLevel, u as ValidationReport, v as acquireLessonsLock, w as addLesson, x as graphFilePath, y as importLegacyLessons, _ as lessonsLockPath, z as lessonsPaths, B as loadLessonsGraph, C as mergeLessons, E as mutateLessonsGraph, F as parseGraph, G as queryLessons, H as rankLessons, J as scaffoldLessons, K as serializeGraph, N as stripLegacyMarkers, O as stripMarkersInGraph, P as toRelPath, Q as tryLoadLessonsGraph, W as validateLessonsGraph } from './init-PvpXanVd.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/lessons.js
CHANGED
|
@@ -9,7 +9,8 @@ import { tmpdir, hostname } from 'os';
|
|
|
9
9
|
import 'timers/promises';
|
|
10
10
|
|
|
11
11
|
// src/lessons/graph-schema.ts
|
|
12
|
-
var CURRENT_GRAPH_VERSION =
|
|
12
|
+
var CURRENT_GRAPH_VERSION = 2;
|
|
13
|
+
var VersionSchema = z.union([z.literal(1), z.literal(2)]);
|
|
13
14
|
var MAX_RULE_LENGTH = 2e3;
|
|
14
15
|
var IdSchema = z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case");
|
|
15
16
|
var DateSchema = z.string().regex(
|
|
@@ -33,10 +34,17 @@ var LessonSchema = z.object({
|
|
|
33
34
|
evidence: z.array(z.string().min(1)),
|
|
34
35
|
status: LessonStatusSchema,
|
|
35
36
|
supersededBy: IdSchema.optional(),
|
|
36
|
-
createdAt: DateSchema
|
|
37
|
+
createdAt: DateSchema,
|
|
38
|
+
/**
|
|
39
|
+
* `'always'` marks an ALWAYS-ON lesson: a universal standard delivered on
|
|
40
|
+
* every task (via the UserPromptSubmit hook / a `--always` recall) rather than
|
|
41
|
+
* matched by triggers. Such a lesson needs no trigger and is excluded from
|
|
42
|
+
* triggered recall. Absent = a normal triggered lesson.
|
|
43
|
+
*/
|
|
44
|
+
scope: z.literal("always").optional()
|
|
37
45
|
}).strict();
|
|
38
46
|
var LessonsGraphSchema = z.object({
|
|
39
|
-
version:
|
|
47
|
+
version: VersionSchema,
|
|
40
48
|
lessons: z.record(IdSchema, LessonSchema),
|
|
41
49
|
topics: z.record(IdSchema, TopicSchema),
|
|
42
50
|
triggers: z.record(IdSchema, TriggerSchema)
|
|
@@ -271,6 +279,12 @@ function collectDeadFileGlobs(graph, findings, knownPaths) {
|
|
|
271
279
|
});
|
|
272
280
|
}
|
|
273
281
|
}
|
|
282
|
+
function fileGlobMatchCount(pattern, knownPaths) {
|
|
283
|
+
const isMatch = picomatch(pattern, { dot: true });
|
|
284
|
+
let n = 0;
|
|
285
|
+
for (const p of knownPaths) if (isMatch(p)) n += 1;
|
|
286
|
+
return n;
|
|
287
|
+
}
|
|
274
288
|
var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
|
|
275
289
|
function collectRunnerAnchoredPatterns(graph, findings) {
|
|
276
290
|
const active = activeTriggerIds(graph);
|
|
@@ -288,7 +302,7 @@ function collectRunnerAnchoredPatterns(graph, findings) {
|
|
|
288
302
|
}
|
|
289
303
|
|
|
290
304
|
// src/lessons/capture-guardrails.ts
|
|
291
|
-
var
|
|
305
|
+
var WIDE_GLOB_MATCH_COUNT = 40;
|
|
292
306
|
var MAX_RECOMMENDED_TRIGGERS = 8;
|
|
293
307
|
function isBroadGlob(pattern) {
|
|
294
308
|
const p = pattern.trim();
|
|
@@ -344,9 +358,19 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
|
344
358
|
message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file in the working tree \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
|
|
345
359
|
});
|
|
346
360
|
}
|
|
361
|
+
const wide = triggers.filter((t) => t.kind === "file_glob" && !isBroadGlob(t.pattern)).filter((t) => fileGlobMatchCount(t.pattern, knownPaths) > WIDE_GLOB_MATCH_COUNT).map((t) => t.pattern);
|
|
362
|
+
if (wide.length > 0) {
|
|
363
|
+
warnings.push({
|
|
364
|
+
code: "WIDE_GLOB_MATCH",
|
|
365
|
+
message: `Lesson "${lessonId}" has file glob(s) (${wide.join(", ")}) matching more than ${WIDE_GLOB_MATCH_COUNT} files in the working tree; narrow to the file-CLASS where the rule actually applies so it does not fire on unrelated edits.`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
347
368
|
}
|
|
348
369
|
return warnings;
|
|
349
370
|
}
|
|
371
|
+
|
|
372
|
+
// src/lessons/capture-near-duplicate.ts
|
|
373
|
+
var NEAR_DUPLICATE_THRESHOLD = 0.6;
|
|
350
374
|
function nearDuplicateWarning(graph, lessonId) {
|
|
351
375
|
const subject = graph.lessons[lessonId];
|
|
352
376
|
if (subject === void 0) return null;
|
|
@@ -575,7 +599,7 @@ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
|
|
|
575
599
|
|
|
576
600
|
Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
|
|
577
601
|
|
|
578
|
-
**Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches. Pure-read commands and recall itself are exempt.
|
|
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.
|
|
579
603
|
|
|
580
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>\`.
|
|
581
605
|
|
|
@@ -644,7 +668,7 @@ async function importLegacyLessons(projectRoot, options) {
|
|
|
644
668
|
if (options.force !== true && populated) {
|
|
645
669
|
throw new LessonsGraphExistsError();
|
|
646
670
|
}
|
|
647
|
-
g.version =
|
|
671
|
+
g.version = CURRENT_GRAPH_VERSION;
|
|
648
672
|
g.lessons = lessons;
|
|
649
673
|
g.topics = topics;
|
|
650
674
|
g.triggers = triggers;
|
|
@@ -832,7 +856,7 @@ function getHostname() {
|
|
|
832
856
|
return hostname();
|
|
833
857
|
}
|
|
834
858
|
function sleep(ms) {
|
|
835
|
-
return new Promise((
|
|
859
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
836
860
|
}
|
|
837
861
|
|
|
838
862
|
// src/lessons/lessons-lock.ts
|
|
@@ -1616,7 +1640,7 @@ function validateLessonsGraph(graph, options = {}) {
|
|
|
1616
1640
|
|
|
1617
1641
|
// src/lessons/mutate.ts
|
|
1618
1642
|
function emptyGraph() {
|
|
1619
|
-
return { version:
|
|
1643
|
+
return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
|
|
1620
1644
|
}
|
|
1621
1645
|
function findingKey(f) {
|
|
1622
1646
|
return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
|
|
@@ -1640,6 +1664,7 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
|
|
|
1640
1664
|
`mutateLessonsGraph: refusing to write \u2014 this change introduces ${errors}. (Pre-existing graph issues are not blocking; run \`agentsmesh lessons validate\` to review and \`lessons untrigger\`/\`prune\` to repair them.)`
|
|
1641
1665
|
);
|
|
1642
1666
|
}
|
|
1667
|
+
graph.version = CURRENT_GRAPH_VERSION;
|
|
1643
1668
|
saveLessonsGraph(projectRoot, graph);
|
|
1644
1669
|
return result;
|
|
1645
1670
|
} finally {
|
|
@@ -1717,14 +1742,15 @@ function addLessonInto(graph, input, options) {
|
|
|
1717
1742
|
}
|
|
1718
1743
|
graph.topics[input.topic] = { summary: options.topicSummary };
|
|
1719
1744
|
}
|
|
1720
|
-
|
|
1745
|
+
const skipTriggerGates = options.allowNoTrigger === true || input.scope === "always";
|
|
1746
|
+
if (!skipTriggerGates) {
|
|
1721
1747
|
const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
|
|
1722
1748
|
if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
|
|
1723
1749
|
throw new NoTriggerError();
|
|
1724
1750
|
}
|
|
1725
1751
|
}
|
|
1726
1752
|
const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
|
|
1727
|
-
if (
|
|
1753
|
+
if (!skipTriggerGates) {
|
|
1728
1754
|
const resultingTriggers = existingId !== null ? union(graph.lessons[existingId].triggers, triggerIds) : triggerIds;
|
|
1729
1755
|
const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
|
|
1730
1756
|
if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
|
|
@@ -1738,7 +1764,9 @@ function addLessonInto(graph, input, options) {
|
|
|
1738
1764
|
topics: union(existing.topics, [input.topic]),
|
|
1739
1765
|
triggers: union(existing.triggers, triggerIds),
|
|
1740
1766
|
evidence: union(existing.evidence, input.evidence ?? []),
|
|
1741
|
-
...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {}
|
|
1767
|
+
...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
|
|
1768
|
+
// Re-capturing a rule with --scope always promotes it to always-on.
|
|
1769
|
+
...input.scope === "always" ? { scope: "always" } : {}
|
|
1742
1770
|
};
|
|
1743
1771
|
return {
|
|
1744
1772
|
id: existingId,
|
|
@@ -1758,7 +1786,8 @@ function addLessonInto(graph, input, options) {
|
|
|
1758
1786
|
evidence: input.evidence === void 0 ? [] : [...input.evidence],
|
|
1759
1787
|
status: "active",
|
|
1760
1788
|
createdAt: input.createdAt ?? todayIso(),
|
|
1761
|
-
...input.rationale === void 0 ? {} : { rationale: input.rationale }
|
|
1789
|
+
...input.rationale === void 0 ? {} : { rationale: input.rationale },
|
|
1790
|
+
...input.scope === "always" ? { scope: "always" } : {}
|
|
1762
1791
|
};
|
|
1763
1792
|
const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
|
|
1764
1793
|
const nearDup = nearDuplicateWarning(graph, id);
|
|
@@ -1920,6 +1949,18 @@ function capJsonl(path, maxRecords) {
|
|
|
1920
1949
|
`, "utf8");
|
|
1921
1950
|
renameSync(tmp, path);
|
|
1922
1951
|
}
|
|
1952
|
+
function readJsonl(path) {
|
|
1953
|
+
if (!existsSync(path)) return [];
|
|
1954
|
+
const out = [];
|
|
1955
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
1956
|
+
if (line.trim().length === 0) continue;
|
|
1957
|
+
try {
|
|
1958
|
+
out.push(JSON.parse(line));
|
|
1959
|
+
} catch {
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return out;
|
|
1963
|
+
}
|
|
1923
1964
|
var MAX_RECALL_LOG_RECORDS = 5e3;
|
|
1924
1965
|
var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
|
|
1925
1966
|
var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
|
|
@@ -2018,14 +2059,19 @@ function listProjectFiles(projectRoot) {
|
|
|
2018
2059
|
}
|
|
2019
2060
|
|
|
2020
2061
|
// src/lessons/keyword-match.ts
|
|
2021
|
-
function splitTokens(text) {
|
|
2022
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
2023
|
-
}
|
|
2024
2062
|
function deriveHaystackTokens(query) {
|
|
2025
2063
|
const parts = [];
|
|
2026
2064
|
if (query.file !== void 0) parts.push(query.file);
|
|
2027
2065
|
if (query.command !== void 0) parts.push(query.command);
|
|
2028
|
-
|
|
2066
|
+
if (parts.length === 0) return [];
|
|
2067
|
+
const out = [];
|
|
2068
|
+
for (const raw of parts.join(" ").split(/[^A-Za-z0-9]+/)) {
|
|
2069
|
+
if (raw.length === 0) continue;
|
|
2070
|
+
out.push(raw.toLowerCase());
|
|
2071
|
+
const sub = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(" ").filter((t) => t.length > 0);
|
|
2072
|
+
if (sub.length > 1) out.push(...sub);
|
|
2073
|
+
}
|
|
2074
|
+
return out;
|
|
2029
2075
|
}
|
|
2030
2076
|
function containsRun(needle, hay) {
|
|
2031
2077
|
if (needle.length === 0) return false;
|
|
@@ -2058,6 +2104,7 @@ function queryLessons(graph, query) {
|
|
|
2058
2104
|
const matched = [];
|
|
2059
2105
|
for (const [id, lesson] of Object.entries(graph.lessons)) {
|
|
2060
2106
|
if (lesson.status !== "active") continue;
|
|
2107
|
+
if (lesson.scope === "always") continue;
|
|
2061
2108
|
if (lesson.triggers.some((t) => matchedTriggerIds.has(t))) {
|
|
2062
2109
|
matched.push({ id, lesson });
|
|
2063
2110
|
}
|
|
@@ -2103,6 +2150,7 @@ var RRF_K = 60;
|
|
|
2103
2150
|
var SPECIFICITY_WEIGHT = 3;
|
|
2104
2151
|
var TOPIC_COHERENCE_WEIGHT = 2;
|
|
2105
2152
|
var BM25_WEIGHT = 1;
|
|
2153
|
+
var EFFECTIVENESS_WEIGHT = 1;
|
|
2106
2154
|
function rankMap(items) {
|
|
2107
2155
|
const sorted = [...items].sort(
|
|
2108
2156
|
(a, b) => b.value !== a.value ? b.value - a.value : a.id < b.id ? -1 : 1
|
|
@@ -2139,16 +2187,20 @@ function rankLessons(graph, query, matches, options = {}) {
|
|
|
2139
2187
|
specificity,
|
|
2140
2188
|
// `id` is a matched lesson, and buildTopicCoherence keys every matched id.
|
|
2141
2189
|
topicCoherence: coherence.get(id),
|
|
2142
|
-
matchedTriggers: hitTriggers
|
|
2190
|
+
matchedTriggers: hitTriggers,
|
|
2191
|
+
// Absent from the log ⇒ neutral 1, so a lesson with no outcome data never
|
|
2192
|
+
// sinks below one that has merely been recorded.
|
|
2193
|
+
effectiveness: options.effectiveness?.get(id) ?? 1
|
|
2143
2194
|
};
|
|
2144
2195
|
});
|
|
2145
2196
|
const bm25Ranks = rankMap(scored.map((s) => ({ id: s.id, value: s.bm25 })));
|
|
2146
2197
|
const specRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.specificity })));
|
|
2147
2198
|
const topicRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.topicCoherence })));
|
|
2199
|
+
const effRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.effectiveness })));
|
|
2148
2200
|
const ranked = scored.map((s) => ({
|
|
2149
2201
|
id: s.id,
|
|
2150
2202
|
lesson: s.lesson,
|
|
2151
|
-
score: SPECIFICITY_WEIGHT / (RRF_K + specRanks.get(s.id)) + TOPIC_COHERENCE_WEIGHT / (RRF_K + topicRanks.get(s.id)) + BM25_WEIGHT / (RRF_K + bm25Ranks.get(s.id)),
|
|
2203
|
+
score: SPECIFICITY_WEIGHT / (RRF_K + specRanks.get(s.id)) + TOPIC_COHERENCE_WEIGHT / (RRF_K + topicRanks.get(s.id)) + BM25_WEIGHT / (RRF_K + bm25Ranks.get(s.id)) + EFFECTIVENESS_WEIGHT / (RRF_K + effRanks.get(s.id)),
|
|
2152
2204
|
reason: {
|
|
2153
2205
|
matchedTriggers: s.matchedTriggers,
|
|
2154
2206
|
bm25: s.bm25,
|
|
@@ -2209,17 +2261,58 @@ function loadRecallConfig(projectRoot) {
|
|
|
2209
2261
|
return fallback;
|
|
2210
2262
|
}
|
|
2211
2263
|
}
|
|
2264
|
+
function outcomeLogPath(projectRoot) {
|
|
2265
|
+
return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
|
|
2266
|
+
}
|
|
2267
|
+
function readOutcomeLog(projectRoot) {
|
|
2268
|
+
return readJsonl(outcomeLogPath(projectRoot));
|
|
2269
|
+
}
|
|
2270
|
+
function scopeKey(ev) {
|
|
2271
|
+
return `${ev.session ?? ""}\0${ev.contextKey}`;
|
|
2272
|
+
}
|
|
2273
|
+
function effectiveness(events) {
|
|
2274
|
+
const lastFailure = /* @__PURE__ */ new Map();
|
|
2275
|
+
events.forEach((ev, i) => {
|
|
2276
|
+
if (ev.kind === "failure") lastFailure.set(scopeKey(ev), i);
|
|
2277
|
+
});
|
|
2278
|
+
const out = /* @__PURE__ */ new Map();
|
|
2279
|
+
events.forEach((ev, i) => {
|
|
2280
|
+
if (ev.kind !== "delivered") return;
|
|
2281
|
+
const cur = out.get(ev.lessonId) ?? { delivered: 0, missed: 0 };
|
|
2282
|
+
cur.delivered += 1;
|
|
2283
|
+
const lf = lastFailure.get(scopeKey(ev));
|
|
2284
|
+
if (lf !== void 0 && lf > i) cur.missed += 1;
|
|
2285
|
+
out.set(ev.lessonId, cur);
|
|
2286
|
+
});
|
|
2287
|
+
return out;
|
|
2288
|
+
}
|
|
2289
|
+
function effectivenessScore(o) {
|
|
2290
|
+
return o.delivered === 0 ? 1 : 1 - o.missed / o.delivered;
|
|
2291
|
+
}
|
|
2292
|
+
function loadEffectiveness(projectRoot) {
|
|
2293
|
+
const map = /* @__PURE__ */ new Map();
|
|
2294
|
+
for (const [id, o] of effectiveness(readOutcomeLog(projectRoot))) {
|
|
2295
|
+
map.set(id, effectivenessScore(o));
|
|
2296
|
+
}
|
|
2297
|
+
return map;
|
|
2298
|
+
}
|
|
2212
2299
|
var SEEN_DIR = "agentsmesh-lessons-seen";
|
|
2213
2300
|
function openSessionDedup(options = {}) {
|
|
2214
2301
|
if (options.disabled === true) return null;
|
|
2215
2302
|
const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
|
|
2216
2303
|
if (id === void 0) return null;
|
|
2217
|
-
const path = seenPath(id);
|
|
2304
|
+
const path = seenPath(id, options.projectRoot);
|
|
2218
2305
|
return { sessionId: id, seen: loadSeen(path), path };
|
|
2219
2306
|
}
|
|
2220
|
-
function
|
|
2307
|
+
function shortHash(value) {
|
|
2308
|
+
let h = 5381;
|
|
2309
|
+
for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
|
|
2310
|
+
return h.toString(36);
|
|
2311
|
+
}
|
|
2312
|
+
function seenPath(id, projectRoot) {
|
|
2221
2313
|
const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 200);
|
|
2222
|
-
|
|
2314
|
+
const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash(resolve(projectRoot))}`;
|
|
2315
|
+
return join(tmpdir(), SEEN_DIR, `${scoped}.json`);
|
|
2223
2316
|
}
|
|
2224
2317
|
function loadSeen(path) {
|
|
2225
2318
|
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
@@ -2265,14 +2358,25 @@ async function recallLessons(projectRoot, query, options = {}) {
|
|
|
2265
2358
|
const graph = load.graph;
|
|
2266
2359
|
const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
|
|
2267
2360
|
const matches = queryLessons(graph, matchQuery);
|
|
2268
|
-
const dedup = openSessionDedup({
|
|
2361
|
+
const dedup = openSessionDedup({
|
|
2362
|
+
explicit: options.sessionId,
|
|
2363
|
+
disabled: options.noDedup,
|
|
2364
|
+
projectRoot
|
|
2365
|
+
});
|
|
2269
2366
|
const forRank = dedup === null ? matches : filterUnseen(dedup, matches);
|
|
2270
2367
|
const cfg = loadRecallConfig(projectRoot);
|
|
2271
2368
|
const lessons = rankLessons(graph, matchQuery, forRank, {
|
|
2272
2369
|
limit: options.limit ?? cfg.limit,
|
|
2273
|
-
maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens
|
|
2370
|
+
maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens,
|
|
2371
|
+
// 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
|
+
effectiveness: loadEffectiveness(projectRoot)
|
|
2274
2374
|
});
|
|
2275
|
-
if (dedup !== null)
|
|
2375
|
+
if (dedup !== null)
|
|
2376
|
+
commitSeen(
|
|
2377
|
+
dedup,
|
|
2378
|
+
lessons.map((l) => l.id)
|
|
2379
|
+
);
|
|
2276
2380
|
recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, { bypassed: false });
|
|
2277
2381
|
return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
|
|
2278
2382
|
}
|
|
@@ -2400,18 +2504,27 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
|
|
|
2400
2504
|
return { changedIds, changedCount: changedIds.length };
|
|
2401
2505
|
}
|
|
2402
2506
|
var RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
|
|
2403
|
-
var
|
|
2404
|
-
var RECALL_EVENTS = [
|
|
2405
|
-
|
|
2507
|
+
var RECALL_HOOK_TOOL_MATCHER = "Edit|Write|Bash";
|
|
2508
|
+
var RECALL_EVENTS = [
|
|
2509
|
+
{ event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
|
|
2510
|
+
{ event: "PostToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
|
|
2511
|
+
{ event: "UserPromptSubmit", matcher: "*" },
|
|
2512
|
+
// Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: only Claude
|
|
2513
|
+
// Code's passthrough hooks emit it; whitelist targets drop it without warning
|
|
2514
|
+
// (BEST_EFFORT_HOOK_EVENTS). PostToolUse is success-only, so failures need this.
|
|
2515
|
+
{ event: "PostToolUseFailure", matcher: "*" },
|
|
2516
|
+
// Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
|
|
2517
|
+
// BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
|
|
2518
|
+
{ event: "SessionStart", matcher: "*" }
|
|
2519
|
+
];
|
|
2520
|
+
function injectEvent(doc, event, matcher) {
|
|
2406
2521
|
const existing = doc.get(event);
|
|
2407
2522
|
const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
|
|
2408
2523
|
const present = seq.items.some(
|
|
2409
2524
|
(item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
|
|
2410
2525
|
);
|
|
2411
2526
|
if (present) return false;
|
|
2412
|
-
seq.add(
|
|
2413
|
-
doc.createNode({ matcher: RECALL_HOOK_MATCHER, type: "command", command: RECALL_HOOK_COMMAND })
|
|
2414
|
-
);
|
|
2527
|
+
seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
|
|
2415
2528
|
doc.set(event, seq);
|
|
2416
2529
|
return true;
|
|
2417
2530
|
}
|
|
@@ -2420,12 +2533,16 @@ function injectRecallHook(projectRoot) {
|
|
|
2420
2533
|
if (!existsSync(path)) return false;
|
|
2421
2534
|
const doc = parseDocument(readFileSync(path, "utf8"));
|
|
2422
2535
|
let changed = false;
|
|
2423
|
-
for (const event of RECALL_EVENTS) {
|
|
2424
|
-
if (injectEvent(doc, event)) changed = true;
|
|
2536
|
+
for (const { event, matcher } of RECALL_EVENTS) {
|
|
2537
|
+
if (injectEvent(doc, event, matcher)) changed = true;
|
|
2425
2538
|
}
|
|
2426
2539
|
if (changed) writeFileSync(path, String(doc), "utf8");
|
|
2427
2540
|
return changed;
|
|
2428
2541
|
}
|
|
2542
|
+
|
|
2543
|
+
// src/lessons/merge-driver-setup.ts
|
|
2544
|
+
var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";
|
|
2545
|
+
var LESSONS_GITATTRIBUTES_ENTRY = `.agentsmesh/lessons/lessons.json merge=${LESSONS_MERGE_DRIVER}`;
|
|
2429
2546
|
var UTF8_BOM = "\uFEFF";
|
|
2430
2547
|
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2431
2548
|
".md",
|
|
@@ -2553,7 +2670,19 @@ async function writeFileAtomic(path, content, options) {
|
|
|
2553
2670
|
}
|
|
2554
2671
|
}
|
|
2555
2672
|
|
|
2556
|
-
// src/utils/filesystem/
|
|
2673
|
+
// src/utils/filesystem/gitattributes.ts
|
|
2674
|
+
async function ensureGitattributesEntries(projectRoot, entries) {
|
|
2675
|
+
const path = join(projectRoot, ".gitattributes");
|
|
2676
|
+
const current = await readFileSafe(path) ?? "";
|
|
2677
|
+
const existing = new Set(
|
|
2678
|
+
current.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("#"))
|
|
2679
|
+
);
|
|
2680
|
+
const toAdd = entries.filter((e) => !existing.has(e.trim()));
|
|
2681
|
+
if (toAdd.length === 0) return false;
|
|
2682
|
+
const suffix = current.endsWith("\n") || current === "" ? "" : "\n";
|
|
2683
|
+
await writeFileAtomic(path, current + suffix + toAdd.join("\n") + "\n");
|
|
2684
|
+
return true;
|
|
2685
|
+
}
|
|
2557
2686
|
async function ensureGitignoreEntries(projectRoot, entries) {
|
|
2558
2687
|
const gitignorePath = join(projectRoot, ".gitignore");
|
|
2559
2688
|
const current = await readFileSafe(gitignorePath) ?? "";
|
|
@@ -2662,23 +2791,37 @@ regression / wrong assumption / surprise and you have not captured (nor stated
|
|
|
2662
2791
|
\`Lesson: none\`)? The task is INCOMPLETE \u2014 and the user will check. The graph
|
|
2663
2792
|
\`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
|
|
2664
2793
|
|
|
2665
|
-
## Recall \u2014 before each
|
|
2794
|
+
## Recall \u2014 before each edit/command, and at task start
|
|
2666
2795
|
|
|
2667
2796
|
\`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule.
|
|
2668
2797
|
Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
|
|
2669
|
-
is the anti-pattern** \u2014
|
|
2798
|
+
for a specific edit is the anti-pattern** \u2014 anchor those to \`--file\`/\`--cmd\`. But at the
|
|
2799
|
+
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 names PLUS the
|
|
2801
|
+
universal always-on lessons \u2014 the manual equivalent of the automatic prompt recall on
|
|
2802
|
+
hook-capable tools. Author a \`keyword\` trigger beside a \`file_glob\` on conceptual lessons
|
|
2803
|
+
so they are reachable both ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`).
|
|
2670
2804
|
|
|
2671
2805
|
## Capture \u2014 Gate Function (before any completion claim)
|
|
2672
2806
|
|
|
2673
2807
|
1. **SELF-CRITIQUE**: any failure, correction, regression, wrong assumption,
|
|
2674
2808
|
useful surprise, repeated friction, or non-obvious fix? Failing
|
|
2675
2809
|
tests/lint/typecheck and user/review corrections \u2014 yours or anyone's \u2014 all count.
|
|
2810
|
+
PROCESS/TOOLING surprises count too (a sandbox command that silently no-op'd, a port
|
|
2811
|
+
already in use, a library's argument shape) \u2014 trigger those with \`--trigger-cmd\`. So
|
|
2812
|
+
does a review finding you REVIEWED then deliberately REJECTED: capture it as a "do NOT
|
|
2813
|
+
refactor" guard so the reasoning survives the session.
|
|
2676
2814
|
2. **CAPTURE** a reusable imperative rule with an effective trigger (else say so):
|
|
2677
|
-
\`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob
|
|
2815
|
+
\`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob>\`. Trigger on the
|
|
2816
|
+
file-CLASS where it will RECUR (e.g. \`**/lib/use*Form*.ts\`), NOT the single file you
|
|
2817
|
+
discovered it in \u2014 and NOT a broad \`src/**\`. If the rule is a UNIVERSAL standard that
|
|
2818
|
+
applies to EVERY task (a comment/test/style convention no file or command names),
|
|
2819
|
+
capture it with \`--scope always\` instead \u2014 it needs no trigger and is delivered on
|
|
2820
|
+
every task.
|
|
2678
2821
|
3. **RECEIPT**: emit \`Lesson: captured <id>\` or \`Lesson: none\`.
|
|
2679
2822
|
|
|
2680
|
-
At least one _effective_ trigger is required or
|
|
2681
|
-
(\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
|
|
2823
|
+
At least one _effective_ trigger is required (or \`--scope always\` for a universal rule) or
|
|
2824
|
+
the capture is rejected (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
|
|
2682
2825
|
\`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
|
|
2683
2826
|
\`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
|
|
2684
2827
|
deprecate, merge, untrigger, strip-markers, prune, journal, validate, stats, import-md.
|
|
@@ -2724,9 +2867,21 @@ async function scaffoldLessons(projectRoot) {
|
|
|
2724
2867
|
const recallHookInjected = injectRecallHook(projectRoot);
|
|
2725
2868
|
const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
|
|
2726
2869
|
toRelPath(projectRoot, recallLogPath(projectRoot)),
|
|
2727
|
-
toRelPath(projectRoot, captureLogPath(projectRoot))
|
|
2870
|
+
toRelPath(projectRoot, captureLogPath(projectRoot)),
|
|
2871
|
+
toRelPath(projectRoot, outcomeLogPath(projectRoot))
|
|
2872
|
+
]);
|
|
2873
|
+
const gitattributesUpdated = await ensureGitattributesEntries(projectRoot, [
|
|
2874
|
+
LESSONS_GITATTRIBUTES_ENTRY
|
|
2728
2875
|
]);
|
|
2729
|
-
return {
|
|
2876
|
+
return {
|
|
2877
|
+
created,
|
|
2878
|
+
updated,
|
|
2879
|
+
skipped,
|
|
2880
|
+
rootRuleUpdated,
|
|
2881
|
+
gitignoreUpdated,
|
|
2882
|
+
gitattributesUpdated,
|
|
2883
|
+
recallHookInjected
|
|
2884
|
+
};
|
|
2730
2885
|
}
|
|
2731
2886
|
function seedLessonsSkill(projectRoot, created, updated, skipped) {
|
|
2732
2887
|
const skillPath = join(projectRoot, ".agentsmesh/skills", LESSONS_SKILL_NAME, "SKILL.md");
|