@lorekit/cli 1.15.0 → 1.16.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/README.md +10 -0
- package/package.json +1 -1
- package/src/control.mjs +22 -0
- package/src/core/lessons.mjs +65 -31
- package/src/doctor.mjs +18 -1
- package/src/hook.mjs +12 -2
package/README.md
CHANGED
|
@@ -483,6 +483,16 @@ Both files share this schema — all fields optional:
|
|
|
483
483
|
// values: "claude" | "cursor" | "codex"
|
|
484
484
|
// repo wins over user
|
|
485
485
|
|
|
486
|
+
"hooks.instructions": {
|
|
487
|
+
"SessionStart": "Focus on migration safety. Treat any lesson tagged 'migration' as high-priority.",
|
|
488
|
+
"PostToolUseFailure": "When recording a failure, always include the exact command and exit code.",
|
|
489
|
+
"Stop": null
|
|
490
|
+
},
|
|
491
|
+
// per-event custom text appended to the hook output.
|
|
492
|
+
// both layers merged: repo instructions first, then user.
|
|
493
|
+
// null (or absent key) means no extra instruction for that event.
|
|
494
|
+
// values: string | null (keys: "SessionStart" | "PostToolUseFailure" | "Stop")
|
|
495
|
+
|
|
486
496
|
// ── Telemetry ──────────────────────────────────────────────────────────────
|
|
487
497
|
"telemetry.disabled": true,
|
|
488
498
|
// team-level opt-out for orgs with a no-telemetry policy
|
package/package.json
CHANGED
package/src/control.mjs
CHANGED
|
@@ -137,6 +137,27 @@ export function resolveControl({
|
|
|
137
137
|
(typeof userConfig['hooks.adapter'] === 'string' && userConfig['hooks.adapter'].trim()) ||
|
|
138
138
|
null;
|
|
139
139
|
|
|
140
|
+
// `hooks.instructions` — per-event custom text appended to the hook output so
|
|
141
|
+
// teams can embed project-specific guidance directly into the injected context.
|
|
142
|
+
// Both layers contribute: repo instructions come first, user instructions follow
|
|
143
|
+
// (same direction as `tags.default` — repo supplements, user personalises).
|
|
144
|
+
// null for a given event means "no custom instruction for that event".
|
|
145
|
+
const HOOK_EVENTS = ['SessionStart', 'PostToolUseFailure', 'Stop'];
|
|
146
|
+
const hooksInstructions = {};
|
|
147
|
+
{
|
|
148
|
+
const repoInstr =
|
|
149
|
+
(repoConfig['hooks.instructions'] && typeof repoConfig['hooks.instructions'] === 'object')
|
|
150
|
+
? repoConfig['hooks.instructions'] : {};
|
|
151
|
+
const userInstr =
|
|
152
|
+
(userConfig['hooks.instructions'] && typeof userConfig['hooks.instructions'] === 'object')
|
|
153
|
+
? userConfig['hooks.instructions'] : {};
|
|
154
|
+
for (const ev of HOOK_EVENTS) {
|
|
155
|
+
const parts = [repoInstr[ev], userInstr[ev]]
|
|
156
|
+
.filter((v) => typeof v === 'string' && v.trim().length > 0);
|
|
157
|
+
hooksInstructions[ev] = parts.length > 0 ? parts.join('\n') : null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
140
161
|
return {
|
|
141
162
|
mode: chosen.mode,
|
|
142
163
|
storeTarget,
|
|
@@ -147,6 +168,7 @@ export function resolveControl({
|
|
|
147
168
|
scopeDefaults,
|
|
148
169
|
hooksDisabled,
|
|
149
170
|
hooksAdapter,
|
|
171
|
+
hooksInstructions,
|
|
150
172
|
};
|
|
151
173
|
}
|
|
152
174
|
|
package/src/core/lessons.mjs
CHANGED
|
@@ -51,19 +51,56 @@ export async function fetchLessons(store, cwd) {
|
|
|
51
51
|
return { scope, lessons: lessons.slice(0, MAX_LESSONS) };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Cap on a lesson's one-line hook in the injected index. Long enough to jog
|
|
55
|
+
// recognition, short enough that N lessons stay a scannable list, not a wall —
|
|
56
|
+
// the full text is always one `memory.read` away.
|
|
57
|
+
const HOOK_LEN = 80;
|
|
58
|
+
|
|
59
|
+
// A lesson's first meaningful line, cleaned into a short recognisable hook:
|
|
60
|
+
// skips leading HTML-comment metadata (`<!-- ... -->`) and markdown heading
|
|
61
|
+
// marks, collapses whitespace, and truncates on a word boundary with an
|
|
62
|
+
// ellipsis — so nothing is ever cut mid-word into noise like "cascades to GE".
|
|
63
|
+
function lessonHook(value, max = HOOK_LEN) {
|
|
64
|
+
let first = '';
|
|
65
|
+
for (const raw of String(value || '').split('\n')) {
|
|
66
|
+
const line = raw.trim();
|
|
67
|
+
if (!line || line.startsWith('<!--')) continue; // skip blanks + meta comments
|
|
68
|
+
first = line.replace(/^#+\s*/, ''); // strip markdown heading marks
|
|
69
|
+
if (first) break;
|
|
70
|
+
}
|
|
71
|
+
first = first.replace(/\s+/g, ' ').trim();
|
|
72
|
+
if (first.length <= max) return first;
|
|
73
|
+
const clipped = first.slice(0, max);
|
|
74
|
+
const lastSpace = clipped.lastIndexOf(' ');
|
|
75
|
+
return `${(lastSpace > max * 0.6 ? clipped.slice(0, lastSpace) : clipped).trimEnd()}…`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Render the SessionStart block as a compact INDEX — one terse line per lesson
|
|
79
|
+
// (scope, key, and a short hook), never the full bodies. This mirrors the
|
|
80
|
+
// lorekit-memory intake rule ("report briefly") and the MEMORY.md index pattern:
|
|
81
|
+
// surface WHAT is known so the agent can `memory.read` the one lesson that turns
|
|
82
|
+
// out to matter, instead of paying for every body up front. Null when empty.
|
|
83
|
+
// `instruction` — an optional extra line appended after the index, sourced from
|
|
84
|
+
// `hooks.instructions.SessionStart` in the control config. Lets teams inject
|
|
85
|
+
// project-specific guidance (e.g. "focus on migration safety") without touching
|
|
86
|
+
// the hook internals. Visible even when there are no lessons.
|
|
87
|
+
export function formatLessons(lessons, scope, { instruction = null } = {}) {
|
|
88
|
+
const noun = lessons && lessons.length === 1 ? 'memory' : 'memories';
|
|
89
|
+
if (!lessons || lessons.length === 0) {
|
|
90
|
+
// No lessons — only emit if there is a custom instruction to show.
|
|
91
|
+
if (!instruction) return null;
|
|
92
|
+
return (
|
|
93
|
+
`LoreKit: 0 ${noun} loaded · ${scope.repoScope || 'this workspace'} ` +
|
|
94
|
+
`— considerations, not rules; read any in full with memory.read.\n\n` +
|
|
95
|
+
`Project instruction: ${instruction}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
57
98
|
const header =
|
|
58
|
-
`LoreKit
|
|
59
|
-
|
|
60
|
-
const body = lessons
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return `- (${l.scope}) ${l.key}: ${first}`;
|
|
64
|
-
})
|
|
65
|
-
.join('\n');
|
|
66
|
-
return `${header}\n${body}`;
|
|
99
|
+
`LoreKit: ${lessons.length} ${noun} loaded · ${scope.repoScope || 'this workspace'} ` +
|
|
100
|
+
`— considerations, not rules; read any in full with memory.read.`;
|
|
101
|
+
const body = lessons.map((l) => `- (${l.scope}) ${l.key} — ${lessonHook(l.value)}`).join('\n');
|
|
102
|
+
const instructionBlock = instruction ? `\n\nProject instruction: ${instruction}` : '';
|
|
103
|
+
return `${header}\n${body}${instructionBlock}`;
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
// Distil a small set of significant, lowercased search TERMS from a tool
|
|
@@ -104,19 +141,16 @@ export function relevantLessons(lessons, terms, cap = MAX_RELEVANT) {
|
|
|
104
141
|
}
|
|
105
142
|
|
|
106
143
|
// Render the relevant-lessons block injected alongside the failure nudge, or
|
|
107
|
-
// null when nothing matched.
|
|
108
|
-
//
|
|
144
|
+
// null when nothing matched. Same compact-index shape as `formatLessons`, with a
|
|
145
|
+
// touch more hook per line (there are at most MAX_RELEVANT and they're directly
|
|
146
|
+
// actionable). Framed as prior art, not a directive.
|
|
109
147
|
export function formatRelevantLessons(lessons) {
|
|
110
148
|
if (!lessons || lessons.length === 0) return null;
|
|
149
|
+
const noun = lessons.length === 1 ? 'memory' : 'memories';
|
|
111
150
|
const header =
|
|
112
|
-
`LoreKit — you've hit something like this before
|
|
113
|
-
|
|
114
|
-
const body = lessons
|
|
115
|
-
.map((l) => {
|
|
116
|
-
const first = String(l.value || '').split('\n')[0].slice(0, 300);
|
|
117
|
-
return `- (${l.scope}) ${l.key}: ${first}`;
|
|
118
|
-
})
|
|
119
|
-
.join('\n');
|
|
151
|
+
`LoreKit: ${lessons.length} related ${noun} — you've hit something like this before ` +
|
|
152
|
+
`(considerations, not rules; read in full with memory.read):`;
|
|
153
|
+
const body = lessons.map((l) => `- (${l.scope}) ${l.key} — ${lessonHook(l.value, 140)}`).join('\n');
|
|
120
154
|
return `${header}\n${body}`;
|
|
121
155
|
}
|
|
122
156
|
|
|
@@ -146,12 +180,12 @@ function tagsHint(writeScope, { tagsDefault = [], scopeDefaults = null } = {}) {
|
|
|
146
180
|
export function retrospectiveNudge(scope, control) {
|
|
147
181
|
const writeScope = scope.repoScope || 'global';
|
|
148
182
|
const hint = tagsHint(writeScope, control);
|
|
183
|
+
const instruction = control && control.hooksInstructions && control.hooksInstructions.Stop
|
|
184
|
+
? `\n\nProject instruction: ${control.hooksInstructions.Stop}` : '';
|
|
149
185
|
return (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`(memory.write to ${writeScope}, phrased as an observation).${hint} ` +
|
|
154
|
-
'If nothing was durable, do nothing.'
|
|
186
|
+
`LoreKit: hit any friction worth remembering — a stuck loop, a repeated ` +
|
|
187
|
+
`failure, a gotcha, a wrong assumption? If so, memory.write to ${writeScope} ` +
|
|
188
|
+
`as an observation; else skip.${hint}${instruction}`
|
|
155
189
|
);
|
|
156
190
|
}
|
|
157
191
|
|
|
@@ -161,11 +195,11 @@ export function retrospectiveNudge(scope, control) {
|
|
|
161
195
|
export function failureNudge(toolName, scope, control) {
|
|
162
196
|
const writeScope = scope.repoScope || 'global';
|
|
163
197
|
const hint = tagsHint(writeScope, control);
|
|
164
|
-
const
|
|
198
|
+
const instruction = control && control.hooksInstructions && control.hooksInstructions.PostToolUseFailure
|
|
199
|
+
? `\n\nProject instruction: ${control.hooksInstructions.PostToolUseFailure}` : '';
|
|
165
200
|
return (
|
|
166
|
-
`LoreKit: the last ${toolName} call failed. If
|
|
167
|
-
|
|
168
|
-
`lorekit-memory (memory.write to ${writeScope}) — ${suffix}`
|
|
201
|
+
`LoreKit: the last ${toolName} call failed. If it's recurring or non-obvious, ` +
|
|
202
|
+
`memory.write to ${writeScope} with the fix so the next run avoids it.${hint}${instruction}`
|
|
169
203
|
);
|
|
170
204
|
}
|
|
171
205
|
|
package/src/doctor.mjs
CHANGED
|
@@ -88,7 +88,24 @@ export async function doctor(args) {
|
|
|
88
88
|
record('warn', 'scope', 'no git remote here — memories fall back to global');
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// 6.
|
|
91
|
+
// 6. Hook instructions — show resolved per-event custom instructions when any are set.
|
|
92
|
+
{
|
|
93
|
+
const instr = control.hooksInstructions || {};
|
|
94
|
+
const EVENTS = ['SessionStart', 'PostToolUseFailure', 'Stop'];
|
|
95
|
+
const configured = EVENTS.filter((ev) => instr[ev]);
|
|
96
|
+
if (configured.length > 0) {
|
|
97
|
+
for (const ev of EVENTS) {
|
|
98
|
+
const text = instr[ev];
|
|
99
|
+
if (text) {
|
|
100
|
+
record('info', `hooks.instructions.${ev}`, c.dim(text.length > 80 ? text.slice(0, 77) + '…' : text));
|
|
101
|
+
} else {
|
|
102
|
+
record('info', `hooks.instructions.${ev}`, c.dim('(not set)'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 7. doctor.require — committed list of checks that MUST pass.
|
|
92
109
|
// Useful as a CI gate: any check in the list that did not pass causes a failure.
|
|
93
110
|
const lorekitJson = readLorekitJson(root);
|
|
94
111
|
const required = (Array.isArray(lorekitJson['doctor.require']) ? lorekitJson['doctor.require'] : [])
|
package/src/hook.mjs
CHANGED
|
@@ -93,9 +93,19 @@ async function run(args) {
|
|
|
93
93
|
if (intent === 'read') {
|
|
94
94
|
if (!firstTimeThisSession(parsed.sessionId, 'read')) return 0;
|
|
95
95
|
const store = createStore(control);
|
|
96
|
-
|
|
96
|
+
// When there is no usable store, we still want to emit a custom instruction
|
|
97
|
+
// if one is configured — so we don't bail out entirely on a missing store.
|
|
98
|
+
const sessionInstruction = control.hooksInstructions && control.hooksInstructions.SessionStart
|
|
99
|
+
? control.hooksInstructions.SessionStart : null;
|
|
100
|
+
if (!store) {
|
|
101
|
+
// No store: emit a minimal header + instruction when present, then return.
|
|
102
|
+
if (sessionInstruction) {
|
|
103
|
+
emit(formatLessons(null, { repoScope: null }, { instruction: sessionInstruction }));
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
97
107
|
const { scope: readScope, lessons } = await fetchLessons(store, root);
|
|
98
|
-
emit(formatLessons(lessons, readScope));
|
|
108
|
+
emit(formatLessons(lessons, readScope, { instruction: sessionInstruction }));
|
|
99
109
|
return 0;
|
|
100
110
|
}
|
|
101
111
|
|