@lorekit/cli 1.12.0 → 1.13.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 +12 -5
- package/bin/lorekit.mjs +14 -13
- package/package.json +1 -1
- package/skill/lorekit-memory/SKILL.md +6 -0
- package/skill/lorekit-setup/SKILL.md +68 -0
- package/skill/lorekit-setup/rules/self-improvement-loops.md +257 -0
- package/src/config.mjs +16 -7
- package/src/doctor.mjs +19 -13
- package/src/install.mjs +17 -13
- package/src/lessons-view.mjs +0 -0
- package/src/store/local.mjs +1 -1
- package/src/uninstall.mjs +15 -8
package/README.md
CHANGED
|
@@ -477,9 +477,11 @@ active deny constraints.
|
|
|
477
477
|
| `LOREKIT_TELEMETRY_TOKEN` | bearer token for telemetry export (overrides the baked-in default) |
|
|
478
478
|
| `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_HEADERS` | override the telemetry OTLP endpoint / headers |
|
|
479
479
|
|
|
480
|
-
## What the
|
|
480
|
+
## What the skills do
|
|
481
481
|
|
|
482
|
-
|
|
482
|
+
`install` scaffolds two skills:
|
|
483
|
+
|
|
484
|
+
The **`lorekit-memory`** skill teaches an agent to:
|
|
483
485
|
|
|
484
486
|
- **Read** scoped lessons at the start of a task, on first navigation into
|
|
485
487
|
unfamiliar code, and before risky operations (narrow-to-broad scope merge).
|
|
@@ -491,11 +493,16 @@ This mirrors the read-on-start / write-on-failure loop of the `aw`
|
|
|
491
493
|
autonomous-workflow agent. See the skill's own `SKILL.md` for the full
|
|
492
494
|
protocol.
|
|
493
495
|
|
|
494
|
-
The
|
|
496
|
+
The **`lorekit-setup`** skill is the authoring counterpart: it teaches an agent
|
|
497
|
+
to wire a self-improvement loop into one of *your own* skills or workflows — the
|
|
498
|
+
two-tier model, the lesson bucket convention, and the entrenchment guards. See
|
|
499
|
+
its `SKILL.md` and `rules/self-improvement-loops.md`.
|
|
500
|
+
|
|
501
|
+
The **skills** are model-invoked (the agent chooses to use them). For a
|
|
495
502
|
**deterministic** guarantee — lessons injected on every session start, a nudge
|
|
496
503
|
on every tool failure — use the framework plugins in [`plugins/`](../../plugins/),
|
|
497
|
-
which fire the `lorekit hook` engine on host lifecycle events. The
|
|
498
|
-
the hooks compose: hooks guarantee the *timing*, the
|
|
504
|
+
which fire the `lorekit hook` engine on host lifecycle events. The skills and
|
|
505
|
+
the hooks compose: hooks guarantee the *timing*, the skills supply the
|
|
499
506
|
*authoring judgment*.
|
|
500
507
|
|
|
501
508
|
## Testing & validating across frameworks
|
package/bin/lorekit.mjs
CHANGED
|
@@ -32,14 +32,14 @@ ${c.bold('Usage')}
|
|
|
32
32
|
npx @lorekit/cli <command> [options]
|
|
33
33
|
|
|
34
34
|
${c.bold('Commands')}
|
|
35
|
-
install Scaffold the lorekit-memory
|
|
36
|
-
and install the deterministic hooks (lessons
|
|
37
|
-
nudge on tool failure + at Stop). Prompts to
|
|
38
|
-
project (.claude) or globally for every project
|
|
39
|
-
--project / --global choose non-interactively,
|
|
40
|
-
the hooks (
|
|
41
|
-
uninstall Reverse install: remove the lorekit-memory
|
|
42
|
-
entry, and the lifecycle hooks for the chosen scope. Surgical —
|
|
35
|
+
install Scaffold the lorekit-memory + lorekit-setup skills, wire the
|
|
36
|
+
LoreKit MCP server, and install the deterministic hooks (lessons
|
|
37
|
+
on SessionStart, a nudge on tool failure + at Stop). Prompts to
|
|
38
|
+
install for this project (.claude) or globally for every project
|
|
39
|
+
(~/.claude); --project / --global choose non-interactively,
|
|
40
|
+
--no-hooks skips the hooks (skills stay model-invoked only).
|
|
41
|
+
uninstall Reverse install: remove the lorekit-memory + lorekit-setup skills,
|
|
42
|
+
the MCP server entry, and the lifecycle hooks for the chosen scope. Surgical —
|
|
43
43
|
other servers, hooks, and settings are left untouched. Prompts
|
|
44
44
|
project vs global; --project / --global choose non-interactively.
|
|
45
45
|
doctor Verify the skill install, MCP connectivity, token, and scope.
|
|
@@ -131,13 +131,14 @@ Run ${c.cyan('lorekit <command> --help')} for command-specific options.
|
|
|
131
131
|
// focused entry instead of the full top-level HELP, so a user only sees the
|
|
132
132
|
// flags that actually apply to what they're running.
|
|
133
133
|
const COMMAND_HELP = {
|
|
134
|
-
install: `${c.bold('lorekit install')} — scaffold the
|
|
134
|
+
install: `${c.bold('lorekit install')} — scaffold the skills, wire the MCP server, install the hooks
|
|
135
135
|
|
|
136
136
|
${c.bold('Usage')}
|
|
137
137
|
npx @lorekit/cli install [options]
|
|
138
138
|
|
|
139
|
-
Scaffolds the lorekit-memory
|
|
140
|
-
|
|
139
|
+
Scaffolds the lorekit-memory (runtime read/write) and lorekit-setup (loop
|
|
140
|
+
authoring) skills, adds the LoreKit MCP server, and wires the deterministic
|
|
141
|
+
hooks (lessons on SessionStart, a nudge on tool failure + at Stop).
|
|
141
142
|
|
|
142
143
|
${c.bold('Options')}
|
|
143
144
|
-d, --dir <path> Target project root (default: current directory)
|
|
@@ -159,8 +160,8 @@ ${c.bold('Examples')}
|
|
|
159
160
|
${c.bold('Usage')}
|
|
160
161
|
npx @lorekit/cli uninstall [options]
|
|
161
162
|
|
|
162
|
-
Removes the lorekit-memory
|
|
163
|
-
Surgical — other servers, hooks, and settings are left untouched.
|
|
163
|
+
Removes the lorekit-memory and lorekit-setup skills, the MCP server entry, and
|
|
164
|
+
the lifecycle hooks. Surgical — other servers, hooks, and settings are left untouched.
|
|
164
165
|
|
|
165
166
|
${c.bold('Options')}
|
|
166
167
|
-d, --dir <path> Target project root (default: current directory)
|
package/package.json
CHANGED
|
@@ -46,6 +46,12 @@ Both jobs run through LoreKit's `memory.*` MCP tools.
|
|
|
46
46
|
If those tools are not connected, this skill is a no-op — say so once and
|
|
47
47
|
continue the task; never block work because memory is unavailable.
|
|
48
48
|
|
|
49
|
+
> **Wiring a loop into your own skill?** This skill does the runtime read/write.
|
|
50
|
+
> To set up a *self-improvement loop* for a skill, workflow, or agent — so it
|
|
51
|
+
> reads its own lessons on every run and promotes proven ones into permanent
|
|
52
|
+
> rules — use the **`lorekit-setup`** skill, the authoring counterpart to this
|
|
53
|
+
> one.
|
|
54
|
+
|
|
49
55
|
> **Modes.** Memory has a controllable backend (`lorekit doctor` shows the
|
|
50
56
|
> resolved one): `remote` (the hosted LoreKit server — the default), `local`
|
|
51
57
|
> (markdown files in two tiers — a per-user **home** tier at `~/.lorekit/` and
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lorekit-setup
|
|
3
|
+
description: >
|
|
4
|
+
Sets up a self-improvement loop for a skill, workflow, or agent using LoreKit,
|
|
5
|
+
so a host gets better across runs by reading its own accumulated lessons at
|
|
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),
|
|
9
|
+
chooses the lesson bucket (tag + key namespace) and scopes, and installs the
|
|
10
|
+
entrenchment guards that stop a learning loop from reinforcing its own
|
|
11
|
+
mistakes. Runtime reading and writing of lessons is the lorekit-memory skill;
|
|
12
|
+
this is the authoring counterpart. Use when giving a host durable cross-run
|
|
13
|
+
memory or wiring a lessons loop. Triggers on "set up memory for my skill",
|
|
14
|
+
"add a self-improvement loop", "give my workflow memory", "make this learn
|
|
15
|
+
from its mistakes", "self-improving memory", "/lorekit-setup".
|
|
16
|
+
user-invocable: true
|
|
17
|
+
argument-hint: '[host-name]'
|
|
18
|
+
license: MIT
|
|
19
|
+
metadata:
|
|
20
|
+
author: mthines
|
|
21
|
+
version: '1.0.0'
|
|
22
|
+
workflow_type: memory-loop-authoring
|
|
23
|
+
tags:
|
|
24
|
+
- lorekit
|
|
25
|
+
- self-improvement
|
|
26
|
+
- memory
|
|
27
|
+
- lessons
|
|
28
|
+
- loop
|
|
29
|
+
- authoring
|
|
30
|
+
- setup
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# LoreKit Setup
|
|
34
|
+
|
|
35
|
+
Give a skill, workflow, or agent a **self-improvement loop**: it reads its own
|
|
36
|
+
accumulated lessons at the start of every run and hardens the proven ones into
|
|
37
|
+
permanent rules — so it gets better the more it runs.
|
|
38
|
+
|
|
39
|
+
This is the **authoring** counterpart to `lorekit-memory`. `lorekit-memory` does
|
|
40
|
+
the runtime read/write of individual lessons; `lorekit-setup` wires the durable
|
|
41
|
+
loop that calls those primitives on a host's behalf. Both run on the same
|
|
42
|
+
LoreKit `memory.*` MCP tools.
|
|
43
|
+
|
|
44
|
+
## The two tiers (in one screen)
|
|
45
|
+
|
|
46
|
+
| Tier | Mechanism | Changes behavior? |
|
|
47
|
+
| ---- | --------- | ----------------- |
|
|
48
|
+
| **Fast (episodic)** | LoreKit lessons in a per-host bucket, read at the start of a run, written on failure | **No** — advisory input only |
|
|
49
|
+
| **Slow (procedural)** | A human-reviewed edit that hardens a recurring lesson into a host rule | **Yes** |
|
|
50
|
+
|
|
51
|
+
A recurrence gate connects them: a lesson that recurs (`seen_count >= 3`) or is
|
|
52
|
+
marked `status=structural` becomes promotion-eligible. Entrenchment guards keep
|
|
53
|
+
the fast tier from reinforcing its own wrong conclusions.
|
|
54
|
+
|
|
55
|
+
## Set up a loop
|
|
56
|
+
|
|
57
|
+
Follow [rules/self-improvement-loops.md](./rules/self-improvement-loops.md).
|
|
58
|
+
It covers: when to add a loop (and when not to), the bucket convention (tag
|
|
59
|
+
`loop::<host>-lessons` + key namespace), scope selection, the lesson schema, the
|
|
60
|
+
read/write steps, the promotion gate, the entrenchment guards, a wiring
|
|
61
|
+
checklist, and an interactive setup flow.
|
|
62
|
+
|
|
63
|
+
If invoked with a `host-name`, set up the loop for that host; otherwise ask
|
|
64
|
+
which skill / workflow / agent the loop is for, then walk the interactive setup.
|
|
65
|
+
|
|
66
|
+
The loop's runtime tier needs LoreKit's `memory.*` tools connected; if they are
|
|
67
|
+
not, the host's loop is a silent no-op (the slow tier — a normal source edit —
|
|
68
|
+
still works). Designing the loop needs no connection.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Setting up a self-improvement loop
|
|
2
|
+
|
|
3
|
+
Use this when someone wants a skill, workflow, or agent to **get better across
|
|
4
|
+
runs** — "give my skill memory", "add a self-improvement loop", "make this
|
|
5
|
+
learn from its mistakes", "set up lessons for this workflow".
|
|
6
|
+
|
|
7
|
+
Reading and writing individual lessons at runtime is the **`lorekit-memory`**
|
|
8
|
+
skill (its `intake` / `retrospective` rules). This skill is one level up: how to
|
|
9
|
+
wire a **durable loop** into a host so it reads its own accumulated lessons at
|
|
10
|
+
the start of every run and records new ones on failure — safely, without the
|
|
11
|
+
loop entrenching its own mistakes. The loop runs on the same `memory.*` MCP
|
|
12
|
+
tools `lorekit-memory` uses.
|
|
13
|
+
|
|
14
|
+
The design has two tiers connected by a recurrence gate. Both run on LoreKit.
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [When to add a loop (and when not to)](#when-to-add-a-loop-and-when-not-to)
|
|
19
|
+
- [The two tiers](#the-two-tiers)
|
|
20
|
+
- [Conventions](#conventions)
|
|
21
|
+
- [Read step (start of every run)](#read-step-start-of-every-run)
|
|
22
|
+
- [Write step (on failure / at the end of a run)](#write-step-on-failure--at-the-end-of-a-run)
|
|
23
|
+
- [Promotion (fast → slow)](#promotion-fast--slow)
|
|
24
|
+
- [Entrenchment guards (do not skip these)](#entrenchment-guards-do-not-skip-these)
|
|
25
|
+
- [Wiring checklist](#wiring-checklist)
|
|
26
|
+
- [Interactive setup](#interactive-setup)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## When to add a loop (and when not to)
|
|
31
|
+
|
|
32
|
+
Add a loop when **all** of these hold:
|
|
33
|
+
|
|
34
|
+
- The host is an **orchestrator or multi-step pipeline** that can fail in
|
|
35
|
+
recurring, classifiable ways (wrong triage, a missed step, a false-green gate).
|
|
36
|
+
- Its failures are about the **host's own process**, not just the user's product.
|
|
37
|
+
- There is somewhere a proven lesson could eventually **harden into a rule** (the
|
|
38
|
+
slow tier below).
|
|
39
|
+
|
|
40
|
+
Do **not** add a loop to:
|
|
41
|
+
|
|
42
|
+
- **One-shot utilities** with no durable cross-run subject — the bookkeeping
|
|
43
|
+
costs more than it returns.
|
|
44
|
+
- **Adversarial / audit steps** (a red-team pass, a fresh-eyes reviewer) — they
|
|
45
|
+
must not be biased by prior runs; that is the whole point of a fresh pass.
|
|
46
|
+
- **Steps that handle secrets or credentials** — routing them through lesson
|
|
47
|
+
extraction is a leak risk.
|
|
48
|
+
|
|
49
|
+
When unsure, default to **no**. A loop can be added later; unwinding an
|
|
50
|
+
entrenched-bias loop is harder.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## The two tiers
|
|
55
|
+
|
|
56
|
+
| Tier | Mechanism | Storage | Changes behavior? | Gate |
|
|
57
|
+
| ---- | --------- | ------- | ----------------- | ---- |
|
|
58
|
+
| **Fast (episodic)** | LoreKit lessons (`memory.*`), read at the start of a run, written on failure / at the end | LoreKit | **No** — advisory input only | none (privacy pre-flight only) |
|
|
59
|
+
| **Slow (procedural)** | A permanent edit to the host's own source / rules | the host skill | **Yes** — a rule / gate / trigger | human review + approval |
|
|
60
|
+
|
|
61
|
+
The fast tier captures a lesson immediately and cheaply. A lesson earns a
|
|
62
|
+
**permanent** change to the host (the slow tier) only once it has **recurred
|
|
63
|
+
across runs** — recurrence is the cheap external signal that the lesson is real,
|
|
64
|
+
not a one-off. This is the episodic → procedural promotion path, with a human
|
|
65
|
+
gate on the slow tier so a single bad run can never rewrite the host.
|
|
66
|
+
|
|
67
|
+
The fast tier is optional: if LoreKit's `memory.*` tools are not connected, the
|
|
68
|
+
loop is a silent no-op (log one line, continue). The slow tier is just editing
|
|
69
|
+
the host and is unaffected.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Conventions
|
|
74
|
+
|
|
75
|
+
Give the loop its own **bucket** so its lessons stay separate from every other
|
|
76
|
+
loop's on the same scopes. A bucket is a tag plus a key namespace:
|
|
77
|
+
|
|
78
|
+
- **Tag:** `loop::<host>-lessons` (e.g. `loop::deploy-lessons`). Reads filter by
|
|
79
|
+
it; writes always carry it.
|
|
80
|
+
- **Key:** `<host>-lessons::<kebab-slug>` (e.g. `deploy-lessons::migration-order`).
|
|
81
|
+
Same `scope` + `key` overwrites in place — the mechanism behind recurrence
|
|
82
|
+
counting.
|
|
83
|
+
|
|
84
|
+
Pick the **scope** per lesson from the standard LoreKit model (`::` is the only
|
|
85
|
+
separator; the `lorekit-memory` skill's scope-resolution reference has the full
|
|
86
|
+
derivation):
|
|
87
|
+
|
|
88
|
+
- **`global`** — universal lessons that should follow the user across every repo.
|
|
89
|
+
- **`repo::{owner}/{repo}`** — lessons specific to this repository.
|
|
90
|
+
|
|
91
|
+
Reserve `branch::` for throwaway notes; a loop normally writes `global` or
|
|
92
|
+
`repo::`.
|
|
93
|
+
|
|
94
|
+
### The lesson record
|
|
95
|
+
|
|
96
|
+
A loop lesson is **procedural** ("how to do better next time"), not a fact about
|
|
97
|
+
the user. Keep the machine-read metadata in a `meta:` comment at the top of the
|
|
98
|
+
`value` so the prose stays readable:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
<!-- meta: seen_count=1 status=active expires=<ISO 8601, ~90 days out> trigger-context="<concrete signal — file glob, task type, tool, error shape>" -->
|
|
102
|
+
|
|
103
|
+
# <one-line lesson title>
|
|
104
|
+
|
|
105
|
+
**What failed:** <concrete observable from the run>
|
|
106
|
+
**Why:** <root cause, if known; "unknown" is allowed>
|
|
107
|
+
**What to do next time:** <prescriptive, actionable, testable instruction>
|
|
108
|
+
**Promotion target:** <the host rule/step this would harden if promoted, or "none">
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`trigger-context` must be **concrete** (globs, task types, tool names, error
|
|
112
|
+
shapes) — never "when it feels relevant" — so the read step can match it
|
|
113
|
+
mechanically. `seen_count`, `status`, and `expires` drive recurrence, promotion,
|
|
114
|
+
and decay.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Read step (start of every run)
|
|
119
|
+
|
|
120
|
+
Read narrow-to-broad, filtered by the bucket tag, and merge:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
memory.list { scope: "repo::{owner}/{repo}", tags: ["loop::<host>-lessons"], limit: 50 } # skips silently if memory.* not connected
|
|
124
|
+
memory.list { scope: "global", tags: ["loop::<host>-lessons"], limit: 50 }
|
|
125
|
+
# when the run names a subsystem / error, add a search:
|
|
126
|
+
memory.search { q: "<keywords>", scopes: ["repo::{owner}/*", "global"], limit: 10 }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then:
|
|
130
|
+
|
|
131
|
+
1. Match each lesson's `trigger-context` against the current run. Consider only
|
|
132
|
+
matches. **Skip any lesson whose `expires` is in the past** — treat it as
|
|
133
|
+
stale.
|
|
134
|
+
2. Apply each matching *"What to do next time"* as a **consideration**, not a
|
|
135
|
+
command — it biases the run unless it conflicts with the user's stated intent
|
|
136
|
+
or a task-specific constraint. On conflict, the user's intent wins; surface it.
|
|
137
|
+
3. On a `repo::` vs `global` collision, the `repo::` lesson wins (closer scope).
|
|
138
|
+
|
|
139
|
+
No consolidation pass is needed — LoreKit owns storage and deduplicates on write.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Write step (on failure / at the end of a run)
|
|
144
|
+
|
|
145
|
+
Trigger on friction: a stuck loop, a repeated failure, a gate that should have
|
|
146
|
+
caught something, a near-miss, a guess that paid off. Not on smooth successes.
|
|
147
|
+
|
|
148
|
+
1. **Classify the scope.** Trigger references a concrete repo path / repo-specific
|
|
149
|
+
package or term → `repo::{owner}/{repo}`. A glob, framework, tool, or task
|
|
150
|
+
type with no repo binding → `global`. When ambiguous, default to `global`.
|
|
151
|
+
2. **Deduplicate**, so a recurrence updates in place instead of piling up:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
memory.search { q: "<key words of the lesson>", scopes: ["repo::{owner}/{repo}", "global"], limit: 10 }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
3. **Write** to the classified scope:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
memory.write {
|
|
161
|
+
scope: "<global | repo::{owner}/{repo}>",
|
|
162
|
+
key: "<host>-lessons::<slug>",
|
|
163
|
+
value: "<the lesson body above>",
|
|
164
|
+
tags: ["loop::<host>-lessons", "source::<trigger>"],
|
|
165
|
+
trigger: "<stuck-loop | command-failure | gotcha | near-miss | assumption-wrong | paid-off | manual>"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Same `scope` + `key` overwrites in place. **A recurrence resolves to an UPDATE
|
|
170
|
+
that increments `seen_count` by 1 and refreshes `expires`** — that is what makes
|
|
171
|
+
recurrence countable and drives promotion. If a lesson you applied at the start
|
|
172
|
+
of the run worked (the failure did not recur), still write the UPDATE:
|
|
173
|
+
successful application is recurrence evidence.
|
|
174
|
+
|
|
175
|
+
The privacy pre-flight is never skipped, autonomous or not: a candidate lesson
|
|
176
|
+
containing a secret, token, credential, or PII is **dropped, not written**. The
|
|
177
|
+
bar is stricter for `repo::` writes — a repo scope is team-visible.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Promotion (fast → slow)
|
|
182
|
+
|
|
183
|
+
After a read or write, a lesson is **promotion-eligible** when either:
|
|
184
|
+
|
|
185
|
+
- `seen_count >= 3` — the same failure recurred across at least three runs, or
|
|
186
|
+
- it is tagged `status=structural` because it reflects a design gap, not a
|
|
187
|
+
one-off.
|
|
188
|
+
|
|
189
|
+
For an eligible lesson, **surface a one-line suggestion — never act silently**:
|
|
190
|
+
|
|
191
|
+
> Lesson "<title>" recurred N times. Promote it to a permanent guard in
|
|
192
|
+
> `<host>`? (edit the host's source / rules behind your normal review.)
|
|
193
|
+
|
|
194
|
+
The promotion target follows the lesson's scope: a `global` lesson hardens the
|
|
195
|
+
**host's own source** (every user of the host benefits); a `repo::` lesson
|
|
196
|
+
hardens the **repo's own rules / docs** (every teammate in that repo benefits).
|
|
197
|
+
Promotion is a normal, human-reviewed edit — LoreKit does not apply it. After a
|
|
198
|
+
successful promotion, write an UPDATE setting `status=promoted` so the lesson
|
|
199
|
+
stops re-suggesting and stands as an audit trail of why the rule exists.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Entrenchment guards (do not skip these)
|
|
204
|
+
|
|
205
|
+
The dominant risk of any reflective-memory loop is **self-reinforcing error**:
|
|
206
|
+
the host wrongly concludes "approach X always fails", stores it, avoids X
|
|
207
|
+
forever, and never gathers the evidence to overturn the false belief. These
|
|
208
|
+
guards are what make the loop safe:
|
|
209
|
+
|
|
210
|
+
1. **Lessons are advisory, never auto-applied to behavior.** A lesson biases a
|
|
211
|
+
run; it can never silently disable a gate, skip a step, or change a limit. The
|
|
212
|
+
only path from a lesson to changed behavior is the human-reviewed slow tier.
|
|
213
|
+
2. **Recurrence gates promotion, not a single run** (`seen_count >= 3`, or an
|
|
214
|
+
explicit `status=structural` marker in the lesson's `meta:` comment).
|
|
215
|
+
3. **Every lesson expires** (default ~90 days from last sighting; the read step
|
|
216
|
+
ignores expired lessons, so stale beliefs decay instead of entrenching).
|
|
217
|
+
4. **Contradiction is surfaced, not silently overwritten** — the dedup search
|
|
218
|
+
finds the prior lesson; a genuine reversal is a reviewed decision.
|
|
219
|
+
5. **The privacy pre-flight is never bypassed** — secrets / PII are dropped, not
|
|
220
|
+
stored.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Wiring checklist
|
|
225
|
+
|
|
226
|
+
To add a loop to a host called `<host>`:
|
|
227
|
+
|
|
228
|
+
- [ ] Pick the bucket: tag `loop::<host>-lessons`, key `<host>-lessons::<slug>`.
|
|
229
|
+
- [ ] Add the **read step** at the start of the host's run (narrow-to-broad
|
|
230
|
+
`memory.list` filtered by the tag; apply matches as considerations; skip
|
|
231
|
+
expired).
|
|
232
|
+
- [ ] Add the **write step** at the host's existing failure / end-of-run points
|
|
233
|
+
(classify scope, `memory.search` to dedup, `memory.write`). No new
|
|
234
|
+
reflection stage — hook the points the host already detects.
|
|
235
|
+
- [ ] Add the **promotion suggestion** when a read/written lesson hits
|
|
236
|
+
`seen_count >= 3` or `status=structural`.
|
|
237
|
+
- [ ] State the **entrenchment guards** so a future maintainer does not "optimize
|
|
238
|
+
them away".
|
|
239
|
+
- [ ] Confirm the loop **degrades silently** when `memory.*` is not connected.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Interactive setup
|
|
244
|
+
|
|
245
|
+
When a user asks to set up self-improving memory for a specific host, walk them
|
|
246
|
+
through it rather than dumping the whole recipe:
|
|
247
|
+
|
|
248
|
+
1. **Confirm it should have a loop** — run the "when to add a loop" test above.
|
|
249
|
+
If the host is a one-shot utility, an adversarial pass, or a secret-handler,
|
|
250
|
+
say so and stop.
|
|
251
|
+
2. **Name the bucket** — `loop::<host>-lessons` from the host's name.
|
|
252
|
+
3. **Locate the read and write points** — the start of a run, and the failure /
|
|
253
|
+
end-of-run points the host already has. Show the two `memory.*` snippets with
|
|
254
|
+
the bucket filled in.
|
|
255
|
+
4. **Write the guards down** in the host's own docs, so they survive future edits.
|
|
256
|
+
5. **Do a dry run** — trigger a failure, confirm a lesson is written to the right
|
|
257
|
+
scope with the right tag, then confirm the next run reads it back.
|
package/src/config.mjs
CHANGED
|
@@ -6,8 +6,17 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
|
|
7
7
|
// packages/cli/ — the installable package root (this file lives in src/).
|
|
8
8
|
export const PKG_ROOT = fileURLToPath(new URL('../', import.meta.url));
|
|
9
|
-
export const SKILL_SOURCE = path.join(PKG_ROOT, 'skill', 'lorekit-memory');
|
|
10
9
|
export const SKILL_NAME = 'lorekit-memory';
|
|
10
|
+
export const SKILL_SOURCE = path.join(PKG_ROOT, 'skill', SKILL_NAME);
|
|
11
|
+
|
|
12
|
+
// Every skill the CLI ships. `lorekit-memory` is the operational read/write
|
|
13
|
+
// loop (kept first as the primary — `SKILL_NAME`/`SKILL_SOURCE` above alias it
|
|
14
|
+
// for back-compat); `lorekit-setup` is the authoring skill that wires a
|
|
15
|
+
// self-improvement loop into a host. install/uninstall/doctor iterate this list.
|
|
16
|
+
export const SKILLS = ['lorekit-memory', 'lorekit-setup'].map((name) => ({
|
|
17
|
+
name,
|
|
18
|
+
source: path.join(PKG_ROOT, 'skill', name),
|
|
19
|
+
}));
|
|
11
20
|
|
|
12
21
|
export function resolveProjectRoot(dir) {
|
|
13
22
|
return path.resolve(dir || process.cwd());
|
|
@@ -62,9 +71,9 @@ export function mcpConfigPath(root, scope = 'project') {
|
|
|
62
71
|
// Where the skill is scaffolded for a given scope:
|
|
63
72
|
// project → <root>/.claude/skills/… (this repo only)
|
|
64
73
|
// global → ~/.claude/skills/… (personal skills, all projects)
|
|
65
|
-
export function skillInstallDir(root, scope = 'project') {
|
|
74
|
+
export function skillInstallDir(root, scope = 'project', name = SKILL_NAME) {
|
|
66
75
|
const base = scope === 'global' ? homeDir() : root;
|
|
67
|
-
return path.join(base, '.claude', 'skills',
|
|
76
|
+
return path.join(base, '.claude', 'skills', name);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
// Claude Code settings file that holds the hooks for a given scope:
|
|
@@ -231,10 +240,10 @@ export function readLorekitServer(root) {
|
|
|
231
240
|
// server / hook / setting intact, so uninstalling never damages a shared
|
|
232
241
|
// ~/.claude.json or settings.json.
|
|
233
242
|
|
|
234
|
-
// Delete
|
|
235
|
-
// .claude/skills
|
|
236
|
-
export function removeSkill(root, scope = 'project') {
|
|
237
|
-
const dest = skillInstallDir(root, scope);
|
|
243
|
+
// Delete a scaffolded skill directory. We own the whole
|
|
244
|
+
// .claude/skills/<name> tree for each skill we ship, so a recursive remove is safe.
|
|
245
|
+
export function removeSkill(root, scope = 'project', name = SKILL_NAME) {
|
|
246
|
+
const dest = skillInstallDir(root, scope, name);
|
|
238
247
|
const removed = fs.existsSync(dest);
|
|
239
248
|
if (removed) fs.rmSync(dest, { recursive: true, force: true });
|
|
240
249
|
return { dest, removed };
|
package/src/doctor.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
SKILLS,
|
|
9
9
|
resolveProjectRoot,
|
|
10
10
|
skillInstallDir,
|
|
11
11
|
readLorekitServer,
|
|
@@ -42,18 +42,24 @@ export async function doctor(args) {
|
|
|
42
42
|
`v${process.versions.node}${major < 18 ? ' — need v18+ for fetch' : ''}`,
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
// 2.
|
|
46
|
-
// locations. `lorekit install --global` writes the
|
|
47
|
-
// repo, so a project-only check reports a healthy
|
|
48
|
-
// found" (exactly the false FAIL a --global setup would
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
// 2. Skills installed — check every skill the CLI ships, in BOTH the project
|
|
46
|
+
// and the global (~/.claude) locations. `lorekit install --global` writes the
|
|
47
|
+
// skill under home, not the repo, so a project-only check reports a healthy
|
|
48
|
+
// global install as "not found" (exactly the false FAIL a --global setup would
|
|
49
|
+
// hit).
|
|
50
|
+
for (const skill of SKILLS) {
|
|
51
|
+
const skillMd = [
|
|
52
|
+
skillInstallDir(root, 'project', skill.name),
|
|
53
|
+
skillInstallDir(root, 'global', skill.name),
|
|
54
|
+
]
|
|
55
|
+
.map((dir) => path.join(dir, 'SKILL.md'))
|
|
56
|
+
.find((p) => fs.existsSync(p));
|
|
57
|
+
if (skillMd) {
|
|
58
|
+
const rel = path.relative(root, skillMd);
|
|
59
|
+
record('pass', `skill ${skill.name}`, rel && !rel.startsWith('..') ? rel : prettyPath(skillMd));
|
|
60
|
+
} else {
|
|
61
|
+
record('fail', `skill ${skill.name}`, 'not found — run `lorekit install`');
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
// 3. Resolved control model — which mode, and who decided it.
|
package/src/install.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
// `lorekit install` — scaffold the
|
|
1
|
+
// `lorekit install` — scaffold the skills and wire the MCP server.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import readline from 'node:readline';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
SKILL_NAME,
|
|
7
|
+
SKILLS,
|
|
9
8
|
resolveProjectRoot,
|
|
10
9
|
skillInstallDir,
|
|
11
10
|
copyDir,
|
|
@@ -73,10 +72,13 @@ export async function install(args) {
|
|
|
73
72
|
token = token || null;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
// 3. Install the skill files.
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
// 3. Install the skill files — every skill the CLI ships.
|
|
76
|
+
const skillResults = SKILLS.map((skill) => {
|
|
77
|
+
const dest = skillInstallDir(root, scope, skill.name);
|
|
78
|
+
const existed = fs.existsSync(path.join(dest, 'SKILL.md'));
|
|
79
|
+
const written = copyDir(skill.source, dest, { force: Boolean(args.force) });
|
|
80
|
+
return { name: skill.name, dest, existed, written };
|
|
81
|
+
});
|
|
80
82
|
|
|
81
83
|
// 4. Wire the MCP config for the chosen scope.
|
|
82
84
|
const remoteUrl = buildRemoteUrl(endpoint, token);
|
|
@@ -100,12 +102,14 @@ export async function install(args) {
|
|
|
100
102
|
|
|
101
103
|
// 5. Report.
|
|
102
104
|
heading('Done');
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
for (const s of skillResults) {
|
|
106
|
+
const skillState = !s.existed
|
|
107
|
+
? 'installed'
|
|
108
|
+
: s.written > 0
|
|
109
|
+
? `updated (${s.written} file(s) written)`
|
|
110
|
+
: 'unchanged — pass --force to overwrite';
|
|
111
|
+
status(s.existed && s.written === 0 ? 'info' : 'pass', `skill ${s.name}`, `${skillState} → ${display(s.dest)}`);
|
|
112
|
+
}
|
|
109
113
|
status('pass', mcpLabel, `${existed ? 'updated' : 'created'} lorekit server → ${display(file)}`);
|
|
110
114
|
|
|
111
115
|
if (!wireHooks) {
|
package/src/lessons-view.mjs
CHANGED
|
Binary file
|
package/src/store/local.mjs
CHANGED
|
@@ -316,7 +316,7 @@ class TwoTierStore {
|
|
|
316
316
|
const projRes = this.projectActive()
|
|
317
317
|
? await this.project.search({ q, scopes, tags })
|
|
318
318
|
: { entries: [] };
|
|
319
|
-
const merged = mergeByKey(projRes.entries, homeRes.entries, (e) => `${e.scope}
|
|
319
|
+
const merged = mergeByKey(projRes.entries, homeRes.entries, (e) => `${e.scope}\x00${e.key}`);
|
|
320
320
|
return { ok: true, entries: merged };
|
|
321
321
|
}
|
|
322
322
|
|
package/src/uninstall.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import readline from 'node:readline';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
SKILLS,
|
|
9
9
|
resolveProjectRoot,
|
|
10
10
|
removeSkill,
|
|
11
11
|
removeMcpServer,
|
|
@@ -51,7 +51,10 @@ export async function uninstall(args) {
|
|
|
51
51
|
// install — never clobber what we can't understand), so we catch and report it
|
|
52
52
|
// cleanly, leaving that file byte-for-byte untouched. Atomic writes
|
|
53
53
|
// (writeFileAtomic) guarantee a config can't be half-written even on a crash.
|
|
54
|
-
const
|
|
54
|
+
const skillSteps = SKILLS.map((skill) => ({
|
|
55
|
+
name: skill.name,
|
|
56
|
+
step: attempt(() => removeSkill(root, scope, skill.name)),
|
|
57
|
+
}));
|
|
55
58
|
const mcp = attempt(() => removeMcpServer(root, scope));
|
|
56
59
|
const hooks = attempt(() => removeClaudeHooks(root, scope));
|
|
57
60
|
|
|
@@ -61,10 +64,12 @@ export async function uninstall(args) {
|
|
|
61
64
|
const mcpLabel = scope === 'global' ? '~/.claude.json' : '.mcp.json';
|
|
62
65
|
|
|
63
66
|
heading('Done');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
for (const { name, step } of skillSteps) {
|
|
68
|
+
report(step, `skill ${name}`, {
|
|
69
|
+
done: (r) => `removed → ${display(r.dest)}`,
|
|
70
|
+
noop: 'not installed — nothing to remove',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
68
73
|
report(mcp, mcpLabel, {
|
|
69
74
|
done: (r) => `lorekit server removed → ${display(r.file)}`,
|
|
70
75
|
noop: 'no lorekit server entry — nothing to remove',
|
|
@@ -74,8 +79,10 @@ export async function uninstall(args) {
|
|
|
74
79
|
noop: 'no lorekit hooks — nothing to remove',
|
|
75
80
|
});
|
|
76
81
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
82
|
+
const skillStepList = skillSteps.map((s) => s.step);
|
|
83
|
+
const failed = [...skillStepList, mcp, hooks].some((s) => !s.ok);
|
|
84
|
+
const any =
|
|
85
|
+
(skillStepList.some((s) => s.result?.removed) || mcp.result?.removed || hooks.result?.removed) && true;
|
|
79
86
|
|
|
80
87
|
if (failed) {
|
|
81
88
|
log(`\n ${c.dim('Some items could not be removed and were left untouched — see above.')}`);
|