@lorekit/cli 1.63.0 → 1.63.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.63.0",
3
+ "version": "1.63.2",
4
4
  "description": "Install the LoreKit shared-memory skill and run health checks for the LoreKit MCP server.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -4,8 +4,10 @@ description: >
4
4
  Sets up a self-improvement loop for a skill, workflow, or agent using LoreKit,
5
5
  so a host gets better across runs by reading its own accumulated lessons at
6
6
  the start of every run and hardening the proven ones into permanent rules.
7
- Designs the two tiers (a fast episodic tier of LoreKit lessons, advisory-only;
8
- a slow procedural tier that promotes a recurring lesson into a host rule),
7
+ Designs the lessons loop (a fast episodic tier of LoreKit lessons, advisory-only;
8
+ a slow procedural tier that promotes a recurring lesson into a host rule; and,
9
+ for the rarer judgement-free, independently-checkable case, a third rung that
10
+ compiles a recurring lesson into a mechanically-enforced CI invariant instead),
9
11
  chooses the lesson bucket (tag + key namespace) and scopes, and installs the
10
12
  entrenchment guards that stop a learning loop from reinforcing its own
11
13
  mistakes. Also covers the non-LLM case: giving a deterministic job (a GitHub
@@ -51,16 +53,23 @@ memory that calls those primitives on a host's behalf. Both run on the same
51
53
  LoreKit store — over the `memory.*` MCP tools for agents, over the `lorekit` CLI
52
54
  or REST for jobs.
53
55
 
54
- ## The two tiers of a lessons loop (in one screen)
56
+ ## The rungs of a lessons loop (in one screen)
55
57
 
56
- | Tier | Mechanism | Changes behavior? |
57
- | ---- | --------- | ----------------- |
58
- | **Fast (episodic)** | LoreKit lessons in a per-host bucket, read at the start of a run, written on failure | **No** — advisory input only |
59
- | **Slow (procedural)** | A human-reviewed edit that hardens a recurring lesson into a host rule | **Yes** |
58
+ The runtime loop below is two tiers, fast and slow; a third, rarer rung sits past
59
+ promotion, for the minority of recurring lessons that can be turned into a
60
+ mechanically-checked rule instead of text a reader has to notice.
60
61
 
61
- A recurrence gate connects them: a lesson that recurs (`seen_count >= 3`) or is
62
- marked `status=structural` becomes promotion-eligible. Entrenchment guards keep
63
- the fast tier from reinforcing its own wrong conclusions.
62
+ | Rung | Mechanism | Changes behavior? | Advisory or enforced? |
63
+ | ---- | --------- | ----------------- | ---------------------- |
64
+ | **Fast (episodic)** | LoreKit lessons in a per-host bucket, read at the start of a run, written on failure | **No** — advisory input only | Advisory |
65
+ | **Slow (procedural)** | A human-reviewed edit that hardens a recurring lesson into a host rule | **Yes** | Advisory (works only if the next reader notices it) |
66
+ | **Compiled invariant** | A declarative `obligations-map.mjs` entry a CI gate checks against a changed-file set — see [rules/compiled-invariants.md](./rules/compiled-invariants.md) | **Yes** | Enforced once `gating`; most lessons never qualify |
67
+
68
+ A recurrence gate connects the first two: a lesson that recurs (`seen_count >= 3`)
69
+ or is marked `status=structural` becomes promotion-eligible. Entrenchment guards
70
+ keep the fast tier from reinforcing its own wrong conclusions. The third rung has
71
+ its own, stricter gate — the compilability test — and most promotion-eligible
72
+ lessons stop at the second rung because they fail it.
64
73
 
65
74
  ## Pick the shape first
66
75
 
@@ -71,6 +80,7 @@ before reading further:
71
80
  | ------------ | ----- | ---- |
72
81
  | A **model-driven** skill, agent, or workflow that fails in recurring, classifiable ways | Prose **lessons** — advisory, recurrence-gated, promotable into rules | [rules/self-improvement-loops.md](./rules/self-improvement-loops.md) |
73
82
  | A **deterministic job** — a GitHub Actions workflow, a cron script, a release pipeline — that needs last-run state | JSON **state records** — authoritative, parsed, one key per fact | [rules/ci-state-records.md](./rules/ci-state-records.md) |
83
+ | A recurring lesson whose failure mode is judgement-free and checkable against an independent source of truth | A **compiled invariant** — a declarative entry a CI gate enforces mechanically, never advisory once `gating` | [rules/compiled-invariants.md](./rules/compiled-invariants.md) |
74
84
 
75
85
  A host can want both, in separate buckets: the state record carries *what is true
76
86
  right now*, the lesson carries *what we learned about it*.
@@ -90,11 +90,17 @@ Three concrete limits make this a hard rule rather than a style preference:
90
90
  - **5 000** active memories per user by default, enforced by a DB trigger.
91
91
  - **120 requests/min** per user across every LoreKit surface.
92
92
 
93
- And one soft limit that bites sooner: the agent SessionStart hook lists each scope
94
- with a **read cap and no tag filter**, ordered by recency. Per-run CI writes are
95
- the most recently updated rows in the repo scope, so they would displace the
96
- lessons the hook exists to inject. Bounded cardinality is what keeps CI records
97
- cheap enough to live in the same scope agents read.
93
+ And one mechanism that matters more than any of the three limits above for
94
+ keeping state records out of the way: the SessionStart digest is built by
95
+ `fetchLessons`, which keeps only entries where `isGeneralLesson(kind)`
96
+ is true `kind == null || kind === 'lesson'`. A record written with `--kind bus`
97
+ or `--kind signal` is excluded from the digest outright, no matter how recent it
98
+ is; it never competes with lessons for the read cap in the first place. The
99
+ exclusion is by `kind`, not by volume — writing a state record does not, by
100
+ itself, protect it. A record left with `kind` unset (`null`) still passes
101
+ `isGeneralLesson` and *does* count toward the digest like any other lesson,
102
+ which is exactly why the Conventions section below requires `--kind` explicitly
103
+ rather than leaving it to infer.
98
104
 
99
105
  ---
100
106
 
@@ -107,9 +113,17 @@ lesson bucket:
107
113
  Deliberately **not** `loop::…`; that prefix is the lessons grammar.
108
114
  - **Key:** `ci-state::<slug>` — e.g. `ci-state::flaky-tests`. One slug per fact.
109
115
  - **Taxonomy:** pass `--kind bus --host ci` explicitly. `kind`/`host` are only
110
- inferred from `loop::` tags, so a `ci::` tag leaves them NULL unless you say so.
111
- Setting them buys `lorekit list --kind bus --host ci` one command that shows
112
- every state record and nothing else.
116
+ inferred from `loop::` tags, so a `ci::` tag leaves them NULL unless you say so
117
+ and a NULL `kind` still passes `isGeneralLesson`, so an un-kinded state
118
+ record renders as a raw JSON blob inline in every session's SessionStart
119
+ digest, right alongside actual lessons. Setting `--kind` is what keeps it out
120
+ of the digest at all, not just what makes it easy to find. It also buys
121
+ `lorekit list --kind bus --host ci` — one command that shows every state
122
+ record and nothing else — but that's secondary to keeping the digest clean.
123
+ The `kind` vocabulary is **closed** — `lesson`, `bus`, `signal` — adding a
124
+ new one is a schema change (`MemoryKindSchema` in `@lorekit/schemas`), so if
125
+ none of the three fits your record, pick whichever is closest rather than
126
+ inventing a fourth.
113
127
 
114
128
  Scope, by what the fact is about:
115
129
 
@@ -401,9 +415,11 @@ do:
401
415
  6. **Version every record.** An unrecognised `v` means fall back to the first-run
402
416
  path and log it — never parse a shape you do not recognise.
403
417
  7. **Remember agents read this scope.** State records land in `repo::` alongside
404
- lessons and will surface in an agent's SessionStart injection. That is the point
405
- (see below) but it means the value should read sensibly to a human skimming
406
- it, and stay small.
418
+ lessons, and a correctly-kinded one (`--kind bus`/`--kind signal`) is excluded
419
+ from SessionStart's automatic digest an agent finds it by explicitly listing
420
+ or reading the scope (see below), not by it appearing unprompted. It still
421
+ means the value should read sensibly to a human or agent skimming it, and stay
422
+ small.
407
423
 
408
424
  ---
409
425
 
@@ -413,8 +429,10 @@ Job-to-job persistence alone does not justify LoreKit over `actions/cache`. What
413
429
  does is that **both sides of the loop read the same store**:
414
430
 
415
431
  - CI writes `ci-state::flaky-tests` deterministically on every run of `main`.
416
- - An agent asked to "fix the flaky tests" reads it at SessionStart the same repo
417
- scope, no extra wiring and starts from the real list instead of re-deriving it.
432
+ - An agent asked to "fix the flaky tests" reads it with `lorekit list --kind bus
433
+ --host ci` (or a direct `memory_read` of the key) the same repo scope, no
434
+ extra wiring, just an explicit lookup instead of the automatic digest — and
435
+ starts from the real list instead of re-deriving it.
418
436
  - The agent's own findings go back as a **lesson** in the `loop::` bucket, in the
419
437
  same repo scope, where the next human and the next agent both see it.
420
438
  - The dashboard shows both, with each state record linked back to the exact
@@ -0,0 +1,183 @@
1
+ # Compiled invariants (the third rung)
2
+
3
+ A lesson that keeps recurring can go two places. [self-improvement-loops.md](./self-improvement-loops.md)
4
+ covers the first: promote it into a **prose rule** — a human-reviewed edit to the
5
+ host's own source or docs, still read by whoever (human or model) happens to look.
6
+ This file covers the second, harder promotion: turning a recurrence into a
7
+ **compiled invariant** — a declarative, checked assertion that a CI gate or a CLI
8
+ command enforces mechanically, whether or not anyone reads the rule that day.
9
+
10
+ ## The three rungs
11
+
12
+ 1. **Lesson** (fast, episodic). Written by a model at the end of a run, read at
13
+ the start of the next one. Advisory only — it biases a future run, it does not
14
+ block one.
15
+ 2. **Prose rule** (slow, procedural). A human-reviewed edit that hardens a
16
+ recurring lesson into the host's own source or docs. Still advisory in the
17
+ sense that nothing *enforces* it — it works only if the next reader (human or
18
+ model) actually reads it.
19
+ 3. **Compiled invariant** (this file). A declarative entry — in this repo,
20
+ `packages/cli/src/shared/obligations-map.mjs` — that `lorekit obligations`
21
+ checks against a changed-file set. Not advisory: a `gating` entry with an
22
+ independent `guard` fails `--strict`.
23
+
24
+ Rungs 1 and 2 are **probabilistic** — they depend on someone noticing, reading,
25
+ and acting on the text at the right moment. Rung 3 is **deterministic** — the
26
+ check runs whether or not anyone was thinking about the rule that day. That is
27
+ the entire reason to climb this far: it is strictly more reliable, and strictly
28
+ more expensive to build and maintain, which is why most lessons never get here.
29
+
30
+ ## The compilability test
31
+
32
+ A lesson is a compile candidate only if **all four** hold:
33
+
34
+ 1. **Trigger detectable without judgement.** The condition that should fire the
35
+ check can be recognized from a file path, a diff shape, or another mechanical
36
+ signal — not "whether this edit is the kind that matters," which needs a
37
+ reader's judgement. This is exactly what the existing "trigger-context must be
38
+ concrete" requirement in the lessons loop exists for: a lesson with a vague
39
+ trigger-context was never going to compile, so demanding concreteness at
40
+ write time is what keeps the door open later.
41
+ 2. **Assertion statable as must-remain-true.** The rule has to be expressible as
42
+ an invariant ("if A changed, B must also be in the changed set"), not a
43
+ preference or a judgement call ("consider simplifying this").
44
+ 3. **Expected value comes from somewhere OTHER than the memory itself.** The
45
+ check needs an independent source of truth to compare against — a second
46
+ file, a generator's output, an existing test. A lesson that only says "X
47
+ should be true" with no external referent has nothing to check X against.
48
+ 4. **An enforcement point exists.** Something already runs at the right time —
49
+ CI, a pre-commit hook, a CLI command a human or agent actually invokes — for
50
+ the check to plug into. A perfectly compilable rule with nowhere to run is
51
+ still stuck at rung 2.
52
+
53
+ Most memories fail this test, and that is the expected, normal outcome — not a
54
+ sign the lesson was low-quality. A lesson can be a perfectly good, permanently
55
+ useful rung-2 prose rule and never qualify for rung 3, because it fails on
56
+ judgement (a design taste) or on having no independent value to check against (a
57
+ process reminder). Compilability is a property of the failure mode, not of how
58
+ important or how recurrent the lesson is.
59
+
60
+ ## The state ladder
61
+
62
+ A compiled entry does not start out enforcing anything. It moves through:
63
+
64
+ ```
65
+ candidate → advisory → gating → retired
66
+ ```
67
+
68
+ - **candidate** — surfaced by `lorekit invariants candidates` (below), not yet a
69
+ map entry. Exists only in a scan's output, never in the repo.
70
+ - **advisory** — a hand-written `obligations-map.mjs` entry (`state: 'advisory'`).
71
+ Reported by `lorekit obligations` on every hit, gates nothing under `--strict`
72
+ (only `--strict-all` counts it, per its own note: "restores the previous
73
+ behaviour of gating on every unmet obligation regardless of state"). This is
74
+ where every new entry starts.
75
+ - **gating** — fails `--strict`. An entry may only be `gating` if it declares an
76
+ independent `guard`: an existing CI spec or script that *already* asserts the
77
+ partnership on its own, so the map entry is surfacing a real check earlier,
78
+ not inventing a new source of truth. An entry with no `guard` stays advisory by
79
+ construction — it would otherwise assert nothing but its own author's belief.
80
+ - **retired** — not checked; kept in the map for provenance (why the entry
81
+ existed, when it stopped applying).
82
+
83
+ Two more rules keep the ladder honest once an entry reaches `gating`:
84
+
85
+ - **Demote on false positives.** Nothing in `lorekit obligations` runs a `guard`
86
+ or writes `state` back — this is a manual policy, not automation. A `gating`
87
+ entry that fires on an edit it should not have (its `guard` disagrees, or a
88
+ human overrides it) is a signal the entry is too broad or the wrong shape —
89
+ a human should demote it back to `advisory` rather than letting the
90
+ disagreement stand.
91
+ - **A review date.** Every entry carries `reviewBy`; an entry with no independent
92
+ guard gets a shorter horizon than one that does, because it is more likely to
93
+ have been an over-generalization from a single incident.
94
+
95
+ ## The cluster layer
96
+
97
+ An `obligations-map.mjs` entry does not name a bare lesson key — it names a
98
+ **recurrence class**, declared in `packages/cli/src/shared/recurrence-clusters.mjs`.
99
+ Read that file's header docblock for the full rationale; the short version is
100
+ that naming the class (rather than one lesson) lets several unmet obligations
101
+ that share a root cause report as one problem, and gives the compile pipeline a
102
+ join point between "a candidates scan over the memory store" and "a human-written
103
+ map entry." `RECURRENCE_CLUSTERS` is a short, deliberately curated list — adding
104
+ one is an assertion that several distinct memories are really the same class, not
105
+ a bucket you reach for per-entry.
106
+
107
+ ## `lorekit invariants candidates`
108
+
109
+ ```
110
+ lorekit invariants candidates [--scope <scope>] [--min-seen-count N] [--json]
111
+ ```
112
+
113
+ A read-only survey over the memory store. It reuses `dedupe`'s Jaccard clustering
114
+ to find near-duplicate lessons, then ranks the resulting clusters by
115
+ `(summed seen_count × distinct scopes)`, descending. A cluster is worth
116
+ reporting when the summed `seen_count` across its members is at least
117
+ `--min-seen-count` (default 3), or any member's `meta` comment already declares a
118
+ non-`active` status. For each candidate it prints every memory the merge would
119
+ collapse — deliberately the default view, not hidden behind `--verbose`, because
120
+ that list *is* the point of the command: a human decides from it whether the
121
+ cluster deserves a hand-written entry.
122
+
123
+ It also reports whether a cluster already resolves to a named recurrence class
124
+ via the same join `dedupe` uses — so a scan can tell you "this cluster of five
125
+ near-duplicate lessons is already the `copies-a-claim` class" instead of leaving
126
+ you to notice by hand.
127
+
128
+ Two things it deliberately does **not** do, on purpose:
129
+
130
+ - **It does not classify trigger-contexts.** A lesson's raw `trigger-context`
131
+ string, when present, is printed verbatim — never interpreted into a
132
+ glob/command/error-shape. Turning "parses into a detectable trigger" from a
133
+ string into an actual predicate is the human step the compile pipeline
134
+ protects; automating it would be exactly the "auto-compile" this pipeline
135
+ refuses to do.
136
+ - **It does not check `compiled_to`.** No such field exists yet — no schema, no
137
+ server support. A candidate that has already been compiled into an
138
+ `obligations-map.mjs` entry can still surface again on a later scan. This is a
139
+ known, named gap, not a silent one: don't assume a candidate you see today
140
+ hasn't already been acted on, and don't build automation on the assumption
141
+ that a scan's output is the full unresolved set.
142
+
143
+ It never auto-compiles and never gates anything — it produces a report; a human
144
+ reads it and hand-writes the `obligations-map.mjs` entry.
145
+
146
+ ## Where declarations live
147
+
148
+ Compiled invariants are declared **in the repo** — `obligations-map.mjs` is
149
+ reviewable, versioned, ordinary source, subject to the same PR review as any
150
+ other code change. They are never stored as memory-store records. The memory
151
+ store is the *input* to the compile pipeline (it is what `lorekit dedupe` and
152
+ `lorekit invariants candidates` scan) but is never on the critical path of a
153
+ gating check — `lorekit obligations` reads only the changed-file set and the
154
+ map, never the store, so a store outage cannot silently disable a gate.
155
+
156
+ ## The dedupe prerequisite
157
+
158
+ This is the most load-bearing sequencing fact in the pipeline: **cluster before
159
+ you count.** The compile pipeline is `cluster → groom-merge → compile candidate →
160
+ invariant`, in that order, and the clustering step is not optional scaffolding —
161
+ it is the thing that makes the `seen_count` threshold meaningful at all.
162
+
163
+ The concrete case that proves it: the ~45 lesson variants behind the
164
+ `copies-a-claim`/`sibling-set` recurrence were not one lesson written 45 times —
165
+ they were **~45 distinct keys**, each written once, each carrying its own
166
+ `seen_count` of 1. A raw scan for `seen_count >= 3` over the ungroomed store would
167
+ have found *nothing*, because no single key ever crossed the threshold on its
168
+ own. Only after `lorekit dedupe`'s Jaccard clustering merged the near-duplicates
169
+ into groups did the pipeline have anything to sum a `seen_count` over. Skipping
170
+ the merge step, or running the candidates scan against an ungroomed store, does
171
+ not just produce a worse result — it produces a false negative that looks like
172
+ "nothing recurs here."
173
+
174
+ ## `compiled_to` is a named gap
175
+
176
+ There is deliberately no `compiled_to` field on a memory record today — no
177
+ schema for it, no server support. Once a lesson has been compiled into an
178
+ `obligations-map.mjs` entry, nothing marks the source memory as resolved, and
179
+ nothing stops it from surfacing again in a later `lorekit invariants candidates`
180
+ scan or a later `dedupe` pass. This is a known gap, not a claim that the loop
181
+ closes end-to-end today — don't describe the pipeline as though a compiled
182
+ candidate is automatically suppressed from future scans, and don't build
183
+ automation that assumes it is.
@@ -267,6 +267,16 @@ Promotion is a normal, human-reviewed edit — LoreKit does not apply it. After
267
267
  successful promotion, write an UPDATE setting `status=promoted` so the lesson
268
268
  stops re-suggesting and stands as an audit trail of why the rule exists.
269
269
 
270
+ A recurring lesson can be promoted a second time, past the prose rule above,
271
+ into a **compiled invariant** — a declarative, mechanically-checked assertion
272
+ instead of text a reader has to notice and act on. Most lessons never qualify
273
+ (the failure mode has to be judgement-free and checkable against an
274
+ independent source of truth); when one does, see
275
+ [compiled-invariants.md](./compiled-invariants.md) for the compilability test,
276
+ the `candidate → advisory → gating → retired` ladder, and
277
+ `lorekit invariants candidates`, the scan that surfaces compile candidates from
278
+ the store.
279
+
270
280
  ---
271
281
 
272
282
  ## Entrenchment guards (do not skip these)