@lorekit/cli 1.67.0 → 1.69.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 +29 -8
- package/package.json +1 -1
- package/skill/lorekit-groom/rules/grooming-pass.md +10 -0
- package/skill/lorekit-memory/SKILL.md +26 -3
- package/skill/lorekit-memory/references/scope-resolution.md +23 -1
- package/skill/lorekit-memory/rules/intake.md +41 -0
- package/skill/lorekit-memory/rules/retrospective.md +7 -0
- package/skill/lorekit-setup/SKILL.md +42 -1
- package/skill/lorekit-setup/rules/ci-state-records.md +1 -1
- package/skill/lorekit-setup/rules/compiled-invariants.md +9 -8
- package/skill/lorekit-setup/rules/self-improvement-loops.md +278 -37
- package/src/commands/invariants.mjs +17 -10
- package/src/commands/show.mjs +244 -3
- package/src/shared/candidates-pure.mjs +94 -5
- package/src/shared/mirror-pairs.mjs +9 -0
- package/src/shared/scope-precedence.mjs +82 -0
- package/src/store/local.mjs +95 -1
- package/src/store/remote.mjs +60 -3
- package/src/surfaces.generated.mjs +14 -17
|
@@ -21,6 +21,8 @@ The design has two tiers connected by a recurrence gate. Both run on LoreKit.
|
|
|
21
21
|
- [Read step (start of every run)](#read-step-start-of-every-run)
|
|
22
22
|
- [Write step (on failure / at the end of a run)](#write-step-on-failure--at-the-end-of-a-run)
|
|
23
23
|
- [The reconcile-on-re-run flow (resolve + record)](#the-reconcile-on-re-run-flow-resolve--record)
|
|
24
|
+
- [Cross-bucket reads (targeted, read-only)](#cross-bucket-reads-targeted-read-only)
|
|
25
|
+
- [Shared codebase-knowledge (the standard cross-loop layer)](#shared-codebase-knowledge-the-standard-cross-loop-layer)
|
|
24
26
|
- [Promotion (fast → slow)](#promotion-fast--slow)
|
|
25
27
|
- [Entrenchment guards (do not skip these)](#entrenchment-guards-do-not-skip-these)
|
|
26
28
|
- [Wiring checklist](#wiring-checklist)
|
|
@@ -69,6 +71,11 @@ across runs** — recurrence is the cheap external signal that the lesson is rea
|
|
|
69
71
|
not a one-off. This is the episodic → procedural promotion path, with a human
|
|
70
72
|
gate on the slow tier so a single bad run can never rewrite the host.
|
|
71
73
|
|
|
74
|
+
Both tiers are **read by people as well as agents** — a teammate browsing the
|
|
75
|
+
dashboard, a reviewer asking why a rule exists. That is why a lesson body is
|
|
76
|
+
plain markdown with no hidden payload; see
|
|
77
|
+
[the lesson record](#the-lesson-record).
|
|
78
|
+
|
|
72
79
|
The fast tier is optional: if LoreKit's `memory.*` tools are not connected, the
|
|
73
80
|
loop is a silent no-op (log one line, continue). The slow tier is just editing
|
|
74
81
|
the host and is unaffected.
|
|
@@ -99,24 +106,115 @@ Reserve `branch::` for throwaway notes; a loop normally writes `global` or
|
|
|
99
106
|
### The lesson record
|
|
100
107
|
|
|
101
108
|
A loop lesson is **procedural** ("how to do better next time"), not a fact about
|
|
102
|
-
the user.
|
|
103
|
-
|
|
109
|
+
the user. The `value` is **markdown a human reads** — in the dashboard, in a
|
|
110
|
+
SessionStart injection, in another agent's context — and nothing else. Write it
|
|
111
|
+
to this shape:
|
|
104
112
|
|
|
105
113
|
```markdown
|
|
106
|
-
|
|
114
|
+
# <one-line takeaway — what to do, not what the lesson is about>
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
**Applies when:** <concrete signal — file glob, task type, tool name, error shape>
|
|
109
117
|
|
|
110
|
-
**What
|
|
118
|
+
**What happened:** <the concrete observable from the run>
|
|
111
119
|
**Why:** <root cause, if known; "unknown" is allowed>
|
|
112
|
-
**
|
|
120
|
+
**Do this instead:** <prescriptive, actionable, testable instruction>
|
|
113
121
|
**Promotion target:** <the host rule/step this would harden if promoted, or "none">
|
|
114
122
|
```
|
|
115
123
|
|
|
116
|
-
`
|
|
117
|
-
shapes) — never "when it feels relevant" — so the read step can match it
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
`Applies when` must be **concrete** (globs, task types, tool names, error
|
|
125
|
+
shapes) — never "when it feels relevant" — so the read step can match it against
|
|
126
|
+
the current run. It is a visible line, not hidden metadata: the reader deciding
|
|
127
|
+
whether a lesson is theirs needs it first, which is why it sits directly under
|
|
128
|
+
the title.
|
|
129
|
+
|
|
130
|
+
#### Never put machine metadata in the body
|
|
131
|
+
|
|
132
|
+
**A lesson body carries no hidden or machine-only payload — no HTML comment, no
|
|
133
|
+
front-matter block, no JSON blob, no `key=value` header.** Every byte of `value`
|
|
134
|
+
must render as prose a person can read. This is not a style preference; a body
|
|
135
|
+
field is the wrong home for each of these on the merits:
|
|
136
|
+
|
|
137
|
+
| Fact | Where it belongs | Why not the body |
|
|
138
|
+
| ---- | ---------------- | ---------------- |
|
|
139
|
+
| **Recurrence count** | `seen_count`, a store column | `memory_write` sets `seen_count = memories.seen_count + 1` on every overwrite. A count written into prose is a snapshot the writer guessed at and nothing ever updates — stale the first time the lesson recurs, and readers of the real counter never see it |
|
|
140
|
+
| **Expiry** | `ttl_days` on the write (`clear_ttl` to make permanent) | Expiry is enforced against the column by `memory.purge_expired` and the read filters. A date in prose expires nothing |
|
|
141
|
+
| **Status** (`structural`, `promoted`) | a `status::<value>` tag | Tags are first-class and filterable — `memory.list { tags: ["status::structural"] }` finds them. Prose is not queryable |
|
|
142
|
+
| **Owning host / bucket kind** | the `host` and `kind` write fields | Both are first-class, inferred from the `loop::<host>-lessons` tag when omitted, and drive the Explorer's own facets |
|
|
143
|
+
| **Provenance** (repo, branch, commit, PR) | `origin_repo` / `origin_branch` / `origin_commit` / `origin_pr` | First-class, and the dashboard renders them as links |
|
|
144
|
+
| **Trigger** (`stuck-loop`, `command-failure`, …) | the `trigger` write field | Already a facet; restating it in prose adds a line and no information |
|
|
145
|
+
|
|
146
|
+
An HTML comment is the worst of both worlds: markdown renders it to *nothing*, so
|
|
147
|
+
a human sees a lesson that starts mid-thought, while the fields inside it silently
|
|
148
|
+
disagree with the store's own columns. If a fact has a first-class home, put it
|
|
149
|
+
there and leave it out of the prose entirely.
|
|
150
|
+
|
|
151
|
+
This rule governs **lesson** bodies. A CI **state record** is a different shape
|
|
152
|
+
on purpose — its whole `value` is a JSON object, authoritative and parsed rather
|
|
153
|
+
than read, with no prose wrapped around it. See
|
|
154
|
+
[ci-state-records.md](./ci-state-records.md).
|
|
155
|
+
|
|
156
|
+
#### Writing rules
|
|
157
|
+
|
|
158
|
+
Enforce these on every write, autonomous or not:
|
|
159
|
+
|
|
160
|
+
- **One lesson per record.** Two takeaways are two keys.
|
|
161
|
+
- **Lead with the takeaway.** The `#` title is the instruction, not the topic —
|
|
162
|
+
"Pass `--node-modules-dir=none` to `deno check`", not "Notes on deno check".
|
|
163
|
+
- **Bold label, then one short paragraph.** No nesting past one list level, no
|
|
164
|
+
sub-headings; the whole record is read at a glance or not at all.
|
|
165
|
+
- **Fence every command or snippet**, with a language tag.
|
|
166
|
+
- **Budget ~1,500 characters.** A lesson longer than a screen is a document, and
|
|
167
|
+
a loop that injects documents crowds out the run it was meant to help. Cut the
|
|
168
|
+
narrative, keep the instruction.
|
|
169
|
+
- **No run residue** — no transcript excerpts, reasoning traces, session or
|
|
170
|
+
correlation IDs, timestamps, or "in this run I…" framing. A lesson is written
|
|
171
|
+
for the *next* run, which has none of that context.
|
|
172
|
+
- **No secrets or PII**, per the privacy pre-flight in the write step.
|
|
173
|
+
|
|
174
|
+
#### Worked example
|
|
175
|
+
|
|
176
|
+
❌ **Don't** — a hidden header, a title that names a topic, and a run narrative:
|
|
177
|
+
|
|
178
|
+
```markdown
|
|
179
|
+
<!-- meta: seen_count=1 status=active expires=2026-12-11 trigger-context="bash: line 5: dash0link: command not found" -->
|
|
180
|
+
# Notes on the Slack tool
|
|
181
|
+
**What failed:** In this run I called `slackSendMessage --args='{ … `dash0link` … }'`
|
|
182
|
+
and got `bash: line 5: dash0link: command not found`, then retried twice with
|
|
183
|
+
different escaping before it worked. Session 4b1c2efe, 2026-09-11.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Everything before the title renders to nothing for a human; `seen_count` and
|
|
187
|
+
`expires` contradict the store the moment the lesson recurs; the title says what
|
|
188
|
+
the lesson is *about* rather than what to do; and the run residue is dead weight
|
|
189
|
+
in every future context window.
|
|
190
|
+
|
|
191
|
+
✅ **Do** — the same lesson, all metadata in its own field:
|
|
192
|
+
|
|
193
|
+
````markdown
|
|
194
|
+
# Send Slack messages via a heredoc, not inline `--args='…'`
|
|
195
|
+
|
|
196
|
+
**Applies when:** shelling out to `slackSendMessage` with text interpolated from
|
|
197
|
+
an external source (ticket titles, PR titles) that may contain backticks or apostrophes.
|
|
198
|
+
|
|
199
|
+
**What happened:** Inline `--args='{ … }'` failed with `bash: dash0link: command not found` —
|
|
200
|
+
the shell expanded backticks in the JSON, and an apostrophe closed the quoted argument early.
|
|
201
|
+
**Why:** Single quotes do not protect the string once the outer shell re-processes it.
|
|
202
|
+
**Do this instead:** Pass the JSON through a quoted heredoc, which needs no escaping:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
tools invoke slack.slackSendMessage --args="$(cat <<'ENDJSON'
|
|
206
|
+
{ "text": "…" }
|
|
207
|
+
ENDJSON
|
|
208
|
+
)"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Promotion target:** the automation prompt's message-building step.
|
|
212
|
+
````
|
|
213
|
+
|
|
214
|
+
Written with `tags: ["loop::<host>-lessons", "source::command-failure"]`,
|
|
215
|
+
`trigger: "command-failure"`, and `ttl_days: 90` — so the recurrence count, the
|
|
216
|
+
expiry, the owning host and the trigger are all queryable, and the body is
|
|
217
|
+
readable start to finish.
|
|
120
218
|
|
|
121
219
|
---
|
|
122
220
|
|
|
@@ -133,10 +231,10 @@ memory.search { q: "<keywords>", scopes: ["repo::{owner}/*", "global"], limit: 1
|
|
|
133
231
|
|
|
134
232
|
Then:
|
|
135
233
|
|
|
136
|
-
1. Match each lesson's
|
|
137
|
-
matches.
|
|
138
|
-
|
|
139
|
-
2. Apply each matching
|
|
234
|
+
1. Match each lesson's **Applies when** line against the current run. Consider
|
|
235
|
+
only matches. Expired lessons do not come back — the store's own TTL drops
|
|
236
|
+
them, so the read never has to filter on a date in the prose.
|
|
237
|
+
2. Apply each matching **Do this instead** line as a **consideration**, not a
|
|
140
238
|
command — it biases the run unless it conflicts with the user's stated intent
|
|
141
239
|
or a task-specific constraint. On conflict, the user's intent wins; surface it.
|
|
142
240
|
3. On a `repo::` vs `global` collision, the `repo::` lesson wins (closer scope).
|
|
@@ -159,23 +257,31 @@ caught something, a near-miss, a guess that paid off. Not on smooth successes.
|
|
|
159
257
|
memory.search { q: "<key words of the lesson>", scopes: ["repo::{owner}/{repo}", "global"], limit: 10 }
|
|
160
258
|
```
|
|
161
259
|
|
|
162
|
-
3. **Write** to the classified scope
|
|
260
|
+
3. **Write** to the classified scope, putting every fact in its own field and
|
|
261
|
+
nothing but prose in `value`:
|
|
163
262
|
|
|
164
263
|
```text
|
|
165
264
|
memory.write {
|
|
166
|
-
scope:
|
|
167
|
-
key:
|
|
168
|
-
value:
|
|
169
|
-
tags:
|
|
170
|
-
trigger:
|
|
265
|
+
scope: "<global | repo::{owner}/{repo}>",
|
|
266
|
+
key: "<host>-lessons::<slug>",
|
|
267
|
+
value: "<the markdown lesson body above — no hidden blocks>",
|
|
268
|
+
tags: ["loop::<host>-lessons", "source::<trigger>"], # + "status::structural" when it is
|
|
269
|
+
trigger: "<stuck-loop | command-failure | gotcha | near-miss | assumption-wrong | paid-off | manual>",
|
|
270
|
+
ttl_days: 90
|
|
171
271
|
}
|
|
172
272
|
```
|
|
173
273
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
274
|
+
`host` and `kind` are inferred from the `loop::<host>-lessons` tag; pass them
|
|
275
|
+
explicitly only when the tag does not carry them. Add the `origin_*` fields
|
|
276
|
+
when the run knows them.
|
|
277
|
+
|
|
278
|
+
Same `scope` + `key` overwrites in place. **A recurrence resolves to an UPDATE:
|
|
279
|
+
the store increments `seen_count` by 1 for you, and re-passing `ttl_days`
|
|
280
|
+
refreshes the expiry** — that is what makes recurrence countable and drives
|
|
281
|
+
promotion. Never hand-write a count into the body to track this; the column is
|
|
282
|
+
the only copy that stays true. If a lesson you applied at the start of the run
|
|
283
|
+
worked (the failure did not recur), still write the UPDATE: successful
|
|
284
|
+
application is recurrence evidence.
|
|
179
285
|
|
|
180
286
|
The privacy pre-flight is never skipped, autonomous or not: a candidate lesson
|
|
181
287
|
containing a secret, token, credential, or PII is **dropped, not written**. The
|
|
@@ -247,12 +353,141 @@ record shape are specified in `agents/shared/rules/comment-relevance-memory.md`.
|
|
|
247
353
|
|
|
248
354
|
---
|
|
249
355
|
|
|
356
|
+
## Cross-bucket reads (targeted, read-only)
|
|
357
|
+
|
|
358
|
+
The default is strict: a loop reads **only its own bucket**, filtered by
|
|
359
|
+
`loop::<host>-lessons`. That isolation is deliberate — it keeps one loop's
|
|
360
|
+
lessons from drowning another's, and lets each read fire at its own cadence
|
|
361
|
+
against its own decision point. Wholesale "read every lesson this repo knows"
|
|
362
|
+
is an **anti-pattern**: it reintroduces exactly the noise the tag split exists
|
|
363
|
+
to prevent, and buries the matches that would have fired.
|
|
364
|
+
|
|
365
|
+
There is **one** shape of cross-bucket read that is safe and worth wiring: a host
|
|
366
|
+
reading **another host's Signal or Knowledge bucket, matched by a structural key,
|
|
367
|
+
strictly read-only**. Wire it only when all four hold:
|
|
368
|
+
|
|
369
|
+
1. **The other bucket is keyed by something structural** — a `symbol@path`, a
|
|
370
|
+
file path, a stable fingerprint — never by prose. A structural key is what
|
|
371
|
+
makes a cross-host read meaningful: it matches the reader's own concrete work
|
|
372
|
+
(the files or symbols it is about to touch), not a vague topic.
|
|
373
|
+
2. **The read is bounded to the reader's current work.** Match the other bucket's
|
|
374
|
+
keys against the paths / symbols this run will actually touch and ignore the
|
|
375
|
+
rest — never load the whole bucket as advice.
|
|
376
|
+
3. **The reader treats it as advisory and re-verifies.** A cross-host fact can be
|
|
377
|
+
stale — it carries the *writer's* `verified_at_sha`, not the reader's. It
|
|
378
|
+
**raises care** (more coverage on a hotspot, design around a known invariant)
|
|
379
|
+
but never lowers a bar, skips a step, or suppresses a finding. An absent
|
|
380
|
+
record is never evidence of safety.
|
|
381
|
+
4. **The reader never writes the other bucket.** Write ownership stays with the
|
|
382
|
+
one owning host; a second writer corrupts its provenance. Cross-host is a
|
|
383
|
+
**read** relationship only.
|
|
384
|
+
|
|
385
|
+
Do **not** cross-read another host's `loop::<host>-lessons`. Lessons are prose
|
|
386
|
+
"how to do better" advice tuned to that host's own decisions; they re-key on
|
|
387
|
+
rephrasing and carry no structural anchor to match against, so a cross-read of
|
|
388
|
+
them is the wholesale anti-pattern above. Only Signal / Knowledge buckets with
|
|
389
|
+
structural keys qualify.
|
|
390
|
+
|
|
391
|
+
LoreKit ships **one** standard instance of this pattern — the shared
|
|
392
|
+
`codebase-knowledge` bucket that every code-touching loop reads and writes. It is
|
|
393
|
+
the mechanism behind the automatic synergy below, and it is what makes a
|
|
394
|
+
structural key worth insisting on: a fixed name plus a `symbol@path` key is what
|
|
395
|
+
lets a loop wired by one person be consumed by a loop wired by another. It is
|
|
396
|
+
specified in full next.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Shared codebase-knowledge (the standard cross-loop layer)
|
|
401
|
+
|
|
402
|
+
The cross-bucket read above becomes **automatic** through one bucket every LoreKit
|
|
403
|
+
loop shares by name: `codebase-knowledge`. This is the reason two skills wired
|
|
404
|
+
independently — by different people, in different sessions, in the same repo —
|
|
405
|
+
still compound: they read and write the *same* repo-scoped, structurally-keyed
|
|
406
|
+
record of what the codebase has taught every loop that touched it. A code-changing
|
|
407
|
+
loop plans and edits with that history in hand instead of blind; and because the
|
|
408
|
+
loops that consume it also feed it, the synergy appears for a user who wired a
|
|
409
|
+
single skill and nothing else.
|
|
410
|
+
|
|
411
|
+
### The bucket
|
|
412
|
+
|
|
413
|
+
| Field | Value |
|
|
414
|
+
| --- | --- |
|
|
415
|
+
| **Tag** | `codebase-knowledge` |
|
|
416
|
+
| **Kind** | `signal` (a durable per-repo filter, read on every run that touches code) |
|
|
417
|
+
| **Scope** | `repo::{owner}/{repo}` — a codebase fact is repo-bound |
|
|
418
|
+
| **TTL** | ~90 days, refreshed on re-verification |
|
|
419
|
+
| **Keys** | `knowledge::<symbol>@<path>` — verified facts about one symbol (an invariant it holds, its consumer/dependent count, a defect it produced before); `hotspot::<path>` — per-file counters (`confirmed`, `regressed`, `missed`) |
|
|
420
|
+
|
|
421
|
+
The keys are **structural** (`symbol@path`, `path`) on purpose: a key survives a
|
|
422
|
+
rename of the *finding* but not a rename of the *code*, which is exactly the
|
|
423
|
+
sensitivity that lets a different loop match it against the files it is about to
|
|
424
|
+
touch. Set `kind: signal` and `host` explicitly on every write — LoreKit infers
|
|
425
|
+
them only from a `loop::` tag, and this bucket is not tagged that way.
|
|
426
|
+
|
|
427
|
+
### Read side — automatic for any code-touching host
|
|
428
|
+
|
|
429
|
+
Wire it at the host's **plan/apply seam** — the moment it has the concrete
|
|
430
|
+
file/symbol list it will change (a plan's File Changes list, an apply pack, a
|
|
431
|
+
fix's target file):
|
|
432
|
+
|
|
433
|
+
```text
|
|
434
|
+
memory.list { scope: "repo::{owner}/{repo}", tags: ["codebase-knowledge"], limit: 100 }
|
|
435
|
+
# keep only hotspot::<path> / knowledge::<symbol>@<path> whose <path> (and <symbol>)
|
|
436
|
+
# this run will actually touch. Apply as PLANNING INPUTS: raise coverage on a
|
|
437
|
+
# hotspot, design around a known invariant / consumer count. Advisory and
|
|
438
|
+
# re-verified against the code — never a reason to skip a step or suppress a finding.
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
This is the read-side contract from [Cross-bucket reads](#cross-bucket-reads-targeted-read-only)
|
|
442
|
+
made concrete: structural match, bounded to this run, advisory, an absent record
|
|
443
|
+
never evidence of safety.
|
|
444
|
+
|
|
445
|
+
### Write side — how the layer fills, and why many writers stay safe
|
|
446
|
+
|
|
447
|
+
A host that **verifies** a structural fact contributes it back, so the next loop
|
|
448
|
+
reads it. This is what makes the synergy automatic even for a user with one skill
|
|
449
|
+
and no dedicated reviewer: the loops that consume the layer also feed it.
|
|
450
|
+
Multi-writer is safe **only** behind this write contract — bake in every bullet,
|
|
451
|
+
or do not wire the write:
|
|
452
|
+
|
|
453
|
+
- **Structural key from a real symbol/path list**, never composed from prose. A
|
|
454
|
+
prose key accumulates nothing and no reader can match it.
|
|
455
|
+
- **`verified_at_sha` on every fact** — the HEAD this run verified it at. It is the
|
|
456
|
+
whole mechanism the next reader uses to decide "fact stands" vs "re-verify"; an
|
|
457
|
+
absent or stale SHA makes the fact permanently unverifiable, and it is dropped.
|
|
458
|
+
- **`source_agent` stamped** — which host verified it. Together with
|
|
459
|
+
`verified_at_sha` this is what makes many writers safe: a reader sees who
|
|
460
|
+
verified what, and when, so no writer silently overwrites another's provenance.
|
|
461
|
+
- **Only what THIS run actually verified**, grounded in the code — never a guess,
|
|
462
|
+
and never a value about a person or a telemetry reading. A fact about code,
|
|
463
|
+
keyed to code.
|
|
464
|
+
- **Merge, never clobber.** Read the existing record first; append to `history[]`
|
|
465
|
+
or increment counters (each capped) and carry the rest through unchanged. A
|
|
466
|
+
clobbered counter is indistinguishable from a first write.
|
|
467
|
+
- **Raise care, never suppress.** These records only raise priority/coverage on a
|
|
468
|
+
file or symbol. They never lower a bar or silence a finding, and an absent record
|
|
469
|
+
is never evidence of safety. Suppression, if a host needs it, is a different
|
|
470
|
+
bucket behind verification (the Signal in the reconcile flow above).
|
|
471
|
+
- **Explicit `kind: signal` + `host`, a TTL, and the privacy pre-flight** — as
|
|
472
|
+
every write in this skill.
|
|
473
|
+
|
|
474
|
+
Because the name is fixed, the read is the same call in every host, and the write
|
|
475
|
+
follows one contract, **any two LoreKit-wired loops in the same repo compound
|
|
476
|
+
automatically** — which is the whole reason to standardize the name instead of
|
|
477
|
+
letting each host invent its own. The reference ecosystem is `agent-skills`: the
|
|
478
|
+
`pr-reviewer` agent is the primary writer (it verifies symbol facts and file
|
|
479
|
+
hotspots during review), and every code-changing host — `aw`, `implement-suggestion`,
|
|
480
|
+
`fix-bug`, `ci-auto-fix` — reads the layer at its plan/apply seam.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
250
484
|
## Promotion (fast → slow)
|
|
251
485
|
|
|
252
486
|
After a read or write, a lesson is **promotion-eligible** when either:
|
|
253
487
|
|
|
254
|
-
- `seen_count >= 3` — the same failure recurred across at least three runs
|
|
255
|
-
|
|
488
|
+
- `seen_count >= 3` — the same failure recurred across at least three runs (read
|
|
489
|
+
the store's column, never a number written into the body), or
|
|
490
|
+
- it carries the `status::structural` tag because it reflects a design gap, not a
|
|
256
491
|
one-off.
|
|
257
492
|
|
|
258
493
|
For an eligible lesson, **surface a one-line suggestion — never act silently**:
|
|
@@ -264,8 +499,8 @@ The promotion target follows the lesson's scope: a `global` lesson hardens the
|
|
|
264
499
|
**host's own source** (every user of the host benefits); a `repo::` lesson
|
|
265
500
|
hardens the **repo's own rules / docs** (every teammate in that repo benefits).
|
|
266
501
|
Promotion is a normal, human-reviewed edit — LoreKit does not apply it. After a
|
|
267
|
-
successful promotion, write an UPDATE
|
|
268
|
-
stops re-suggesting and stands as an audit trail of why the rule exists.
|
|
502
|
+
successful promotion, write an UPDATE adding the `status::promoted` tag so the
|
|
503
|
+
lesson stops re-suggesting and stands as an audit trail of why the rule exists.
|
|
269
504
|
|
|
270
505
|
A recurring lesson can be promoted a second time, past the prose rule above,
|
|
271
506
|
into a **compiled invariant** — a declarative, mechanically-checked assertion
|
|
@@ -290,9 +525,10 @@ guards are what make the loop safe:
|
|
|
290
525
|
run; it can never silently disable a gate, skip a step, or change a limit. The
|
|
291
526
|
only path from a lesson to changed behavior is the human-reviewed slow tier.
|
|
292
527
|
2. **Recurrence gates promotion, not a single run** (`seen_count >= 3`, or an
|
|
293
|
-
explicit `status
|
|
294
|
-
3. **Every lesson expires**
|
|
295
|
-
|
|
528
|
+
explicit `status::structural` tag on the lesson).
|
|
529
|
+
3. **Every lesson expires** — `ttl_days: 90` on the write, refreshed on each
|
|
530
|
+
recurrence, so stale beliefs decay instead of entrenching. The store enforces
|
|
531
|
+
this; an expiry stated only in prose is decoration.
|
|
296
532
|
4. **Contradiction is surfaced, not silently overwritten** — the dedup search
|
|
297
533
|
finds the prior lesson; a genuine reversal is a reviewed decision.
|
|
298
534
|
5. **The privacy pre-flight is never bypassed** — secrets / PII are dropped, not
|
|
@@ -306,16 +542,21 @@ To add a loop to a host called `<host>`:
|
|
|
306
542
|
|
|
307
543
|
- [ ] Pick the bucket: tag `loop::<host>-lessons`, key `<host>-lessons::<slug>`.
|
|
308
544
|
- [ ] Add the **read step** at the start of the host's run (narrow-to-broad
|
|
309
|
-
`memory.list` filtered by the tag; apply matches as considerations
|
|
310
|
-
expired).
|
|
545
|
+
`memory.list` filtered by the tag; apply matches as considerations).
|
|
311
546
|
- [ ] Add the **write step** at the host's existing failure / end-of-run points
|
|
312
|
-
(classify scope, `memory.search` to dedup, `memory.write`).
|
|
313
|
-
reflection stage — hook the points the host already detects.
|
|
547
|
+
(classify scope, `memory.search` to dedup, `memory.write` with `ttl_days`).
|
|
548
|
+
No new reflection stage — hook the points the host already detects.
|
|
549
|
+
- [ ] State the **body contract** in the host's own write step: markdown to the
|
|
550
|
+
shape above, no hidden blocks, every store-backed fact in its own field.
|
|
314
551
|
- [ ] Add the **promotion suggestion** when a read/written lesson hits
|
|
315
|
-
`seen_count >= 3` or `status
|
|
552
|
+
`seen_count >= 3` or carries `status::structural`.
|
|
316
553
|
- [ ] State the **entrenchment guards** so a future maintainer does not "optimize
|
|
317
554
|
them away".
|
|
318
555
|
- [ ] Confirm the loop **degrades silently** when `memory.*` is not connected.
|
|
556
|
+
- [ ] If the host **touches code**, wire the **[codebase-knowledge](#shared-codebase-knowledge-the-standard-cross-loop-layer)
|
|
557
|
+
read** at its plan/apply seam (match `hotspot::<path>` /
|
|
558
|
+
`knowledge::<symbol>@<path>` to the files it will change). If it **verifies**
|
|
559
|
+
a structural fact, wire the **write** behind that section's contract.
|
|
319
560
|
|
|
320
561
|
---
|
|
321
562
|
|
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Criteria (pure scoring in `../shared/candidates-pure.mjs`):
|
|
16
16
|
// - summed seen_count across a cluster's members >= --min-seen-count
|
|
17
|
-
// (default 3), OR any member
|
|
18
|
-
//
|
|
17
|
+
// (default 3), OR any member already declares a non-"active" status —
|
|
18
|
+
// a `status::<value>` tag, or a legacy `<!-- meta: ... status=... -->`
|
|
19
|
+
// comment for lessons written before tags were the convention
|
|
19
20
|
// - ranked by (summed seen_count × distinct scopes), descending
|
|
20
21
|
//
|
|
21
22
|
// What this deliberately does NOT do:
|
|
22
|
-
// - classify
|
|
23
|
-
//
|
|
24
|
-
// printed verbatim, never interpreted — "parses into
|
|
25
|
-
// trigger" is the human step the compile pipeline protects.
|
|
23
|
+
// - classify an applicability signal into a glob/command/error-shape. The raw
|
|
24
|
+
// string (a lesson's `**Applies when:**` line, or a legacy meta comment's
|
|
25
|
+
// `trigger-context`) is printed verbatim, never interpreted — "parses into
|
|
26
|
+
// a detectable trigger" is the human step the compile pipeline protects.
|
|
26
27
|
// - check `compiled_to`. No such field exists yet (no schema, no server
|
|
27
28
|
// support — see the kickoff's Open Questions), so a candidate already
|
|
28
29
|
// compiled into an obligations-map.mjs entry can still surface here. A
|
|
@@ -150,7 +151,13 @@ function buildCandidates(entries, { threshold, minSeenCount }) {
|
|
|
150
151
|
...cl,
|
|
151
152
|
members: cl.members.map((m) => {
|
|
152
153
|
const raw = byAddress.get(`${m.scope}::${m.key}`);
|
|
153
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
scope: m.scope,
|
|
156
|
+
key: m.key,
|
|
157
|
+
seenCount: seenCountOf(raw),
|
|
158
|
+
value: raw?.value ?? '',
|
|
159
|
+
tags: Array.isArray(raw?.tags) ? raw.tags : [],
|
|
160
|
+
};
|
|
154
161
|
}),
|
|
155
162
|
}));
|
|
156
163
|
return rankCandidates(clusters, { minSeenCount, resolveClass: resolveRecurrenceClass });
|
|
@@ -221,7 +228,7 @@ async function candidates(args) {
|
|
|
221
228
|
heading('LoreKit invariants candidates');
|
|
222
229
|
log(` project: ${c.dim(root)}`);
|
|
223
230
|
log(` scopes: ${scopes.join(' → ')}`);
|
|
224
|
-
log(` ${c.dim(`criteria: summed seen_count >= ${minSeenCount}, or a member's
|
|
231
|
+
log(` ${c.dim(`criteria: summed seen_count >= ${minSeenCount}, or a member's status is non-"active"`)}`);
|
|
225
232
|
|
|
226
233
|
if (offlineSection.available && offlineSection.popCapped) {
|
|
227
234
|
log(` ${c.yellow('!')} population cap (${POP_CAP}) reached for Offline — results are partial. Narrow with --key-prefix, --since, or --max.`);
|
|
@@ -281,8 +288,8 @@ function renderSection(header, section) {
|
|
|
281
288
|
}
|
|
282
289
|
for (const m of cand.members) {
|
|
283
290
|
const fields = [`seen_count=${m.seenCount}`];
|
|
284
|
-
if (m.
|
|
285
|
-
if (m.
|
|
291
|
+
if (m.status) fields.push(`status=${m.status}`);
|
|
292
|
+
if (m.appliesWhen) fields.push(`applies-when=${JSON.stringify(m.appliesWhen)}`);
|
|
286
293
|
log(` ${c.cyan('-')} ${m.scope}::${m.key} ${c.dim(`(${fields.join(', ')})`)}`);
|
|
287
294
|
}
|
|
288
295
|
}
|