@lorekit/cli 1.45.0 → 1.46.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 +20 -0
- package/package.json +1 -1
- package/src/control.mjs +49 -0
- package/src/core/lessons.mjs +17 -7
- package/src/hook.mjs +4 -1
package/README.md
CHANGED
|
@@ -717,6 +717,26 @@ Both files share this schema — all fields optional:
|
|
|
717
717
|
// memories are RANKED before the budget is spent, so what
|
|
718
718
|
// survives is the most-recurring and most-recent, not the newest
|
|
719
719
|
|
|
720
|
+
"hooks.sessionStart.loopCap": 2,
|
|
721
|
+
// how many memories one self-improvement loop (a
|
|
722
|
+
// "loop::<bucket>" tag) may contribute to that block
|
|
723
|
+
// (default 2, bounded 0–40; 0 excludes loop buckets
|
|
724
|
+
// entirely so only general memories are read). Clamped,
|
|
725
|
+
// not rejected; repo wins over user with the same
|
|
726
|
+
// declared-value-owns-the-layer rule as maxChars
|
|
727
|
+
|
|
728
|
+
"hooks.sessionStart.branchHint": "on",
|
|
729
|
+
// whether the block is nudged toward the current git
|
|
730
|
+
// branch's topic — on "feat/embedding-pipeline",
|
|
731
|
+
// embedding memories are lifted (the leading type/author
|
|
732
|
+
// segment is ignored). Default "on"; it only ever lifts
|
|
733
|
+
// an on-topic memory, never buries one. "off" restores
|
|
734
|
+
// the plain most-recurring / most-recent read. Repo wins
|
|
735
|
+
// over user, but — following hooks.userPrompt, not the
|
|
736
|
+
// maxChars layer-lock — a declared-but-unparseable repo
|
|
737
|
+
// value FALLS THROUGH to a valid user value rather than
|
|
738
|
+
// owning the layer
|
|
739
|
+
|
|
720
740
|
"hooks.adapter": "claude",
|
|
721
741
|
// explicit adapter when auto-detection is ambiguous
|
|
722
742
|
// values: "claude" | "cursor" | "codex"
|
package/package.json
CHANGED
package/src/control.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// hooks.stop — Stop-hook gating ("friction" default | "always" | "off")
|
|
11
11
|
// hooks.sessionStart — injected-block shape ("hybrid" default | "index" | "map")
|
|
12
12
|
// hooks.sessionStart.maxChars — character budget for that block (default 1500)
|
|
13
|
+
// hooks.sessionStart.loopCap — max lessons per self-improvement loop bucket (default 2; 0 excludes them)
|
|
14
|
+
// hooks.sessionStart.branchHint — nudge the read toward the git branch topic ("on" default | "off")
|
|
13
15
|
// hooks.userPrompt — the per-turn relevance pull ("on" default | "off")
|
|
14
16
|
// hooks.adapter — explicit adapter override ("claude" | "cursor" | "codex")
|
|
15
17
|
//
|
|
@@ -140,6 +142,31 @@ export function normalizeSessionStartMaxChars(v) {
|
|
|
140
142
|
return i;
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
// The default per-loop-bucket cap for the SessionStart read, and the bounds a
|
|
146
|
+
// configured one is held to. 2 keeps each self-improvement loop's top couple of
|
|
147
|
+
// lessons without letting one bucket flood a general session; 0 is a meaningful
|
|
148
|
+
// setting — exclude loop buckets entirely and read only general codebase lessons.
|
|
149
|
+
// The ceiling is a generous backstop against a typo'd cap, not a shared constant:
|
|
150
|
+
// `core/lessons.mjs` bounds the whole read at its own hard lesson ceiling
|
|
151
|
+
// downstream, so any loopCap at or above that never binds regardless of the exact
|
|
152
|
+
// number here — they are deliberately independent, not kept in lockstep.
|
|
153
|
+
export const DEFAULT_SESSION_START_LOOP_CAP = 2;
|
|
154
|
+
export const MIN_SESSION_START_LOOP_CAP = 0;
|
|
155
|
+
export const MAX_SESSION_START_LOOP_CAP = 40;
|
|
156
|
+
|
|
157
|
+
// Clamp a configured loop cap into range, or null when it is not a usable number
|
|
158
|
+
// (absent, a bare string, NaN). Total: the caller substitutes the default for
|
|
159
|
+
// null. Out-of-range CLAMPS rather than rejecting, like the maxChars budget — and
|
|
160
|
+
// `0` is honoured, not floored away, because "exclude loop buckets" is a real ask.
|
|
161
|
+
export function normalizeSessionStartLoopCap(v) {
|
|
162
|
+
const n = firstNumber(v);
|
|
163
|
+
if (n === null) return null;
|
|
164
|
+
const i = Math.round(n);
|
|
165
|
+
if (i < MIN_SESSION_START_LOOP_CAP) return MIN_SESSION_START_LOOP_CAP;
|
|
166
|
+
if (i > MAX_SESSION_START_LOOP_CAP) return MAX_SESSION_START_LOOP_CAP;
|
|
167
|
+
return i;
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
// A config value that is meant to be a number, or null when it is absent or is
|
|
144
171
|
// something else entirely. Numeric strings are accepted because JSON configs get
|
|
145
172
|
// hand-edited; the RANGE check happens later, at the point of use.
|
|
@@ -328,6 +355,26 @@ export function resolveControl({
|
|
|
328
355
|
const hooksSessionStartMaxChars =
|
|
329
356
|
normalizeSessionStartMaxChars(sessionStartMaxCharsRaw) ?? DEFAULT_SESSION_START_MAX_CHARS;
|
|
330
357
|
|
|
358
|
+
// `hooks.sessionStart.loopCap` — how many lessons one `loop::<bucket>` may
|
|
359
|
+
// contribute. Same layer-before-parse rule as maxChars (declaresScalar): a repo
|
|
360
|
+
// that declared a cap owns it even when the value is garbage, so two people on
|
|
361
|
+
// the same commit get the same read. `0` is a valid, deliberate value, so the
|
|
362
|
+
// default is only substituted when NOTHING usable was declared.
|
|
363
|
+
const sessionStartLoopCapRaw = declaresScalar(repoConfig['hooks.sessionStart.loopCap'])
|
|
364
|
+
? repoConfig['hooks.sessionStart.loopCap']
|
|
365
|
+
: userConfig['hooks.sessionStart.loopCap'];
|
|
366
|
+
const normalizedLoopCap = normalizeSessionStartLoopCap(sessionStartLoopCapRaw);
|
|
367
|
+
const hooksSessionStartLoopCap =
|
|
368
|
+
normalizedLoopCap === null ? DEFAULT_SESSION_START_LOOP_CAP : normalizedLoopCap;
|
|
369
|
+
|
|
370
|
+
// `hooks.sessionStart.branchHint` — whether the read is nudged toward the git
|
|
371
|
+
// branch topic. On/off (the `hooks.userPrompt` vocabulary), default `on`, repo
|
|
372
|
+
// layer wins. Off restores the pre-branch-query read: recency + salience only.
|
|
373
|
+
const hooksSessionStartBranchHint =
|
|
374
|
+
normalizeUserPromptMode(repoConfig['hooks.sessionStart.branchHint']) ||
|
|
375
|
+
normalizeUserPromptMode(userConfig['hooks.sessionStart.branchHint']) ||
|
|
376
|
+
'on';
|
|
377
|
+
|
|
331
378
|
// `hooks.adapter` — repo layer wins over user layer (explicit project override).
|
|
332
379
|
const hooksAdapter =
|
|
333
380
|
(typeof repoConfig['hooks.adapter'] === 'string' && repoConfig['hooks.adapter'].trim()) ||
|
|
@@ -368,6 +415,8 @@ export function resolveControl({
|
|
|
368
415
|
hooksUserPrompt,
|
|
369
416
|
hooksSessionStart,
|
|
370
417
|
hooksSessionStartMaxChars,
|
|
418
|
+
hooksSessionStartLoopCap,
|
|
419
|
+
hooksSessionStartBranchHint,
|
|
371
420
|
hooksAdapter,
|
|
372
421
|
hooksInstructions,
|
|
373
422
|
};
|
package/src/core/lessons.mjs
CHANGED
|
@@ -75,7 +75,9 @@ const HARD_LESSON_CEILING = 40;
|
|
|
75
75
|
// back by their own host through a tag filter. Two per bucket keeps the signal
|
|
76
76
|
// (a loop's top couple of lessons still surface) without the flood; general,
|
|
77
77
|
// non-loop lessons are never capped. Bounded, not shaped: on a store with no
|
|
78
|
-
// loop lessons it never binds.
|
|
78
|
+
// loop lessons it never binds. This is the DEFAULT — a repo/user can override it
|
|
79
|
+
// with `hooks.sessionStart.loopCap` (0 excludes loop buckets entirely), which
|
|
80
|
+
// `fetchLessons` receives as its `loopCap` option.
|
|
79
81
|
const SESSION_START_LOOP_CAP = 2;
|
|
80
82
|
|
|
81
83
|
// How many lessons ride along with the scope map in `map` mode. Small on
|
|
@@ -115,7 +117,11 @@ export const SCOPE_READ_LIMIT = 25;
|
|
|
115
117
|
// that already hold a resolved scope and for tests that need a deterministic
|
|
116
118
|
// branch (deriveScope shells out to git, so the ambient branch — often a
|
|
117
119
|
// detached `HEAD` in CI — cannot exercise the branch-seeded read otherwise).
|
|
118
|
-
export async function fetchLessons(
|
|
120
|
+
export async function fetchLessons(
|
|
121
|
+
store,
|
|
122
|
+
cwd,
|
|
123
|
+
{ now = Date.now(), scope: scopeOverride = null, loopCap = SESSION_START_LOOP_CAP, branchHint = true } = {},
|
|
124
|
+
) {
|
|
119
125
|
const scope = scopeOverride || deriveScope(cwd);
|
|
120
126
|
// Issued BEFORE the per-scope read loop and awaited after it. Nothing in the
|
|
121
127
|
// inventory depends on the loop, so awaiting it afterwards would cost a
|
|
@@ -197,11 +203,12 @@ export async function fetchLessons(store, cwd, { now = Date.now(), scope: scopeO
|
|
|
197
203
|
// objective, and scored with different options those would not line up with the
|
|
198
204
|
// sorted order. `k` is diversification-only; `scopeOrder` is ranking-only and
|
|
199
205
|
// simply ignored by the diversifier's destructuring.
|
|
200
|
-
const rankOpts = sessionRankOpts(scope, now);
|
|
206
|
+
const rankOpts = sessionRankOpts(scope, now, { branchHint });
|
|
201
207
|
const ranked = rankLessons(winners, rankOpts);
|
|
202
208
|
|
|
203
209
|
// AUDIENCE CAP before diversification: no single self-improvement loop may
|
|
204
|
-
// take more than `
|
|
210
|
+
// take more than `loopCap` (the `hooks.sessionStart.loopCap` option, default
|
|
211
|
+
// `SESSION_START_LOOP_CAP`) of the injected slots, so a general
|
|
205
212
|
// session is not flooded with one bot's private `loop::<bucket>` bookkeeping.
|
|
206
213
|
// General (non-loop) lessons pass through uncapped — they are what the cap
|
|
207
214
|
// frees room for. Applied to the ranked list so the survivors are each
|
|
@@ -217,7 +224,7 @@ export async function fetchLessons(store, cwd, { now = Date.now(), scope: scopeO
|
|
|
217
224
|
// which the bounded read never fetched. Reaching those is the recency-window
|
|
218
225
|
// limit (the `order=rank` CANDIDATE_LIMIT problem, one scope down), not this
|
|
219
226
|
// cap's to solve; the cap still does its job of unflooding across scopes.
|
|
220
|
-
const capped = capPerBucket(ranked, { cap:
|
|
227
|
+
const capped = capPerBucket(ranked, { cap: loopCap, bucketOf: loopBucketOf });
|
|
221
228
|
|
|
222
229
|
// ── the scope map: EXACT counts when the store can enumerate ───────────────
|
|
223
230
|
//
|
|
@@ -587,9 +594,12 @@ export function branchQueryTerms(scope) {
|
|
|
587
594
|
// the branch never matched. `fetchLessons` feeds the ONE object this returns to
|
|
588
595
|
// both `rankLessons` and (spread) the diversifier, so their scores agree on
|
|
589
596
|
// terms and the clock.
|
|
590
|
-
export function sessionRankOpts(scope, now) {
|
|
597
|
+
export function sessionRankOpts(scope, now, { branchHint = true } = {}) {
|
|
591
598
|
return {
|
|
592
|
-
|
|
599
|
+
// `branchHint: false` (config `hooks.sessionStart.branchHint: off`) restores
|
|
600
|
+
// the pre-branch-query read — no terms, so relevance contributes nothing and
|
|
601
|
+
// the order is recency + salience.
|
|
602
|
+
terms: branchHint ? branchQueryTerms(scope) : [],
|
|
593
603
|
now,
|
|
594
604
|
scopeOrder: scope && scope.readOrder ? scope.readOrder : null,
|
|
595
605
|
};
|
package/src/hook.mjs
CHANGED
|
@@ -115,7 +115,10 @@ async function run(args) {
|
|
|
115
115
|
}
|
|
116
116
|
return 0;
|
|
117
117
|
}
|
|
118
|
-
const { scope: readScope, lessons, scopeCounts, applicable } = await fetchLessons(store, root
|
|
118
|
+
const { scope: readScope, lessons, scopeCounts, applicable } = await fetchLessons(store, root, {
|
|
119
|
+
loopCap: control.hooksSessionStartLoopCap,
|
|
120
|
+
branchHint: control.hooksSessionStartBranchHint !== 'off',
|
|
121
|
+
});
|
|
119
122
|
emit(formatLessons(lessons, readScope, {
|
|
120
123
|
instruction: sessionInstruction,
|
|
121
124
|
mode: control.hooksSessionStart,
|