@lorekit/cli 1.68.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-memory/SKILL.md +20 -2
- package/skill/lorekit-memory/references/scope-resolution.md +23 -1
- package/skill/lorekit-setup/SKILL.md +25 -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 +144 -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
package/README.md
CHANGED
|
@@ -277,6 +277,25 @@ missing. It exits **non-zero** when the key is found in no readable store, so it
|
|
|
277
277
|
fits scripts. `--json` emits the full normalized record(s) and which store each
|
|
278
278
|
came from. Both a scope and a key are required (else a usage error).
|
|
279
279
|
|
|
280
|
+
#### Batch mode: several references in one call
|
|
281
|
+
|
|
282
|
+
Pass **two or more** references and `show` fetches all of them in one pass
|
|
283
|
+
instead of one invocation per lesson:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
lorekit show global::prefer-guard-clauses repo::acme/widget::build-flags --json
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Each positional must itself parse as a complete `<scope>::<key>` reference —
|
|
290
|
+
that is what tells this apart from the existing `show <scope> <key>`
|
|
291
|
+
two-positional form, whose first positional is a **bare** scope with no `::`.
|
|
292
|
+
Results are reported in the same order the references were given, and
|
|
293
|
+
`--json` emits `{ "results": [{ "scope", "key", "offline", "remote", "diverged" }, …] }`
|
|
294
|
+
instead of the single-reference top-level shape. The remote half of a batch is
|
|
295
|
+
ONE round trip (`POST /memories/read`), not one request per reference; the
|
|
296
|
+
offline half still resolves each reference through the two-tier store, so a
|
|
297
|
+
project-tier lesson still shadows a same-key home-tier one.
|
|
298
|
+
|
|
280
299
|
### Addressing a memory: `<scope::key>`
|
|
281
300
|
|
|
282
301
|
`show`, `write` and `link` all take a memory the same way, and the single-token
|
|
@@ -523,10 +542,11 @@ lorekit invariants candidates --scope repo::owner/repo
|
|
|
523
542
|
```
|
|
524
543
|
|
|
525
544
|
A cluster is a candidate when the **summed `seen_count`** across its members
|
|
526
|
-
is at least `--min-seen-count` (default `3`), or any member
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
545
|
+
is at least `--min-seen-count` (default `3`), or any member already declares a
|
|
546
|
+
non-`"active"` status. Status is read from a `status::<value>` **tag** — the
|
|
547
|
+
canonical home, per the `lorekit-setup` skill's `self-improvement-loops.md` —
|
|
548
|
+
falling back to a legacy `<!-- meta: … status=… -->` body comment for lessons
|
|
549
|
+
written before tags were the convention; the tag wins when both are present.
|
|
530
550
|
Candidates are ranked by (summed `seen_count` × distinct scopes), descending.
|
|
531
551
|
**For each candidate it prints every memory the merge would collapse** —
|
|
532
552
|
that list is the whole point of the command.
|
|
@@ -536,10 +556,11 @@ whose members already resolve to a named recurrence class is flagged as a
|
|
|
536
556
|
stronger case ("this should join an existing invariant") than a merely
|
|
537
557
|
similar one ("this might be a new class").
|
|
538
558
|
|
|
539
|
-
It deliberately does **not** classify a
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
It deliberately does **not** classify a member's applicability signal — its
|
|
560
|
+
visible `**Applies when:**` body line, or a legacy `trigger-context` meta
|
|
561
|
+
field — into a glob/command/error-shape. That judgment is the human step the
|
|
562
|
+
compile pipeline's "never auto-compile, never auto-gate" rule protects, so the
|
|
563
|
+
raw string is printed, never interpreted — and it does **not** know about
|
|
543
564
|
`compiled_to` (no such field exists yet, so an already-compiled candidate can
|
|
544
565
|
still surface here — a known, named gap, not a silent omission).
|
|
545
566
|
|
package/package.json
CHANGED
|
@@ -120,9 +120,9 @@ Full resolution rules: [references/scope-resolution.md](./references/scope-resol
|
|
|
120
120
|
|
|
121
121
|
| Tool | Use | Token |
|
|
122
122
|
|------|-----|-------|
|
|
123
|
-
| `memory.list` | List lessons for one scope (newest first, tag filter) | read |
|
|
123
|
+
| `memory.list` | List lessons for one scope (newest first, tag filter). `scope` is optional — omit it to list across every scope you can see | read |
|
|
124
124
|
| `memory.search` | Full-text search across scopes (supports `repo::owner/*`) | read |
|
|
125
|
-
| `memory.read` | Read one lesson by scope
|
|
125
|
+
| `memory.read` | Read one lesson by key (`scope` optional — see below) — or several at once via `refs: ["scope::key", …]` (one round trip, up to 32) | read |
|
|
126
126
|
| `memory.write` | Store or update a lesson (same scope+key updates in place) | read+write |
|
|
127
127
|
|
|
128
128
|
Write tools need write permission (`lk_rw_*` or `lk_wo_*`); read tools need read
|
|
@@ -130,6 +130,24 @@ permission (`lk_rw_*` or `lk_ro_*`). A read-only token cannot write and a
|
|
|
130
130
|
write-only token cannot read — if a call fails with an authorization error,
|
|
131
131
|
report it and move on; do not retry.
|
|
132
132
|
|
|
133
|
+
Holding only a bare key, with no scope? Call `memory.read { key }` without a
|
|
134
|
+
`scope`. It resolves across every scope you can see, preferring the most
|
|
135
|
+
specific — `project`, then `branch`, then `repo`, then `global` — and the
|
|
136
|
+
response's `scope` field names the one that answered. If the key also exists
|
|
137
|
+
elsewhere, `other_scopes` lists those: that is your cue to pass an explicit
|
|
138
|
+
`scope` next time, because only you know which one you meant. An omitted scope
|
|
139
|
+
does NOT mean `global`. The same applies to `memory.list` and
|
|
140
|
+
`memory.list_archived`, which simply list across everything instead of
|
|
141
|
+
resolving to one winner.
|
|
142
|
+
|
|
143
|
+
Already know the exact `scope::key` refs you need — from a prior `memory.list`
|
|
144
|
+
or `memory.search`, or from citations another lesson names — and need more than
|
|
145
|
+
one? Pass them all as `memory.read`'s `refs` array instead of one call per
|
|
146
|
+
lesson. Each ref resolves independently: an unresolved one lands in the
|
|
147
|
+
response's `missing` list rather than failing the whole call, and matching is
|
|
148
|
+
against the **verbatim stored scope** (no case-folding), unlike a single
|
|
149
|
+
`scope`/`key` read.
|
|
150
|
+
|
|
133
151
|
Every lesson this skill writes carries the tag `skill::lorekit-memory` plus a
|
|
134
152
|
`source::<trigger>` tag (for example `source::stuck-loop`) so lessons are
|
|
135
153
|
easy to find and audit later.
|
|
@@ -37,6 +37,27 @@ Query most specific first, then merge; more specific scopes win on key collision
|
|
|
37
37
|
branch::{owner}/{repo}::{branch} → repo::{owner}/{repo} → project::{name} → global
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Reading without a scope
|
|
41
|
+
|
|
42
|
+
`memory.read`, `memory.list` and `memory.list_archived` all take an **optional**
|
|
43
|
+
`scope`. Omitting it does not mean `global` — it means "every scope I can see".
|
|
44
|
+
|
|
45
|
+
`memory.read { key }` resolves the key across all of them and returns ONE
|
|
46
|
+
lesson, preferring the most specific scope type:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
project::{name} → branch::{owner}/{repo}::{branch} → repo::{owner}/{repo} → global
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
(Same precedence as the read order above, ranked by scope TYPE rather than by
|
|
53
|
+
your own scopes — a hosted call has no working directory to derive them from.)
|
|
54
|
+
Ties within a type break by most-recently-updated. The response's `scope` names
|
|
55
|
+
the scope that answered; `other_scopes`, when present, lists the ones it
|
|
56
|
+
shadowed — pass an explicit `scope` next time if that is not the one you meant.
|
|
57
|
+
|
|
58
|
+
`memory.list` / `memory.list_archived` simply widen instead: every entry carries
|
|
59
|
+
its own `scope`, and nothing is resolved away.
|
|
60
|
+
|
|
40
61
|
## Write scope (retrospective)
|
|
41
62
|
|
|
42
63
|
Use the narrowest scope that correctly describes where the lesson applies:
|
|
@@ -54,7 +75,8 @@ Use the narrowest scope that correctly describes where the lesson applies:
|
|
|
54
75
|
```
|
|
55
76
|
|
|
56
77
|
Wildcards work **only** in `memory.search` — not in `memory.list`,
|
|
57
|
-
`memory.read`, or `memory.write`.
|
|
78
|
+
`memory.read`, or `memory.write`. On the two read tools, omitting `scope`
|
|
79
|
+
entirely (above) is the way to reach past a single scope.
|
|
58
80
|
|
|
59
81
|
## Validation rules
|
|
60
82
|
|
|
@@ -66,7 +66,7 @@ mechanically-checked rule instead of text a reader has to notice.
|
|
|
66
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
67
|
|
|
68
68
|
A recurrence gate connects the first two: a lesson that recurs (`seen_count >= 3`)
|
|
69
|
-
or
|
|
69
|
+
or carries the `status::structural` tag becomes promotion-eligible. Entrenchment guards
|
|
70
70
|
keep the fast tier from reinforcing its own wrong conclusions. The third rung has
|
|
71
71
|
its own, stricter gate — the compilability test — and most promotion-eligible
|
|
72
72
|
lessons stop at the second rung because they fail it.
|
|
@@ -93,6 +93,30 @@ It covers: when to add a loop (and when not to), the bucket convention (tag
|
|
|
93
93
|
read/write steps, the promotion gate, the entrenchment guards, a wiring
|
|
94
94
|
checklist, and an interactive setup flow.
|
|
95
95
|
|
|
96
|
+
### A lesson body is markdown for humans — nothing hidden
|
|
97
|
+
|
|
98
|
+
Whatever host you wire, its write step carries one non-negotiable contract: the
|
|
99
|
+
`value` is **markdown a person reads**, and it carries no HTML comment, no
|
|
100
|
+
front-matter, no JSON blob, and no `key=value` header. Every fact the store
|
|
101
|
+
already models goes in its own write field, never restated in the prose:
|
|
102
|
+
|
|
103
|
+
| Fact | Field, not prose |
|
|
104
|
+
| ---- | ---------------- |
|
|
105
|
+
| Recurrence | `seen_count` — the store increments it on every overwrite |
|
|
106
|
+
| Expiry | `ttl_days` (`clear_ttl` to make permanent) |
|
|
107
|
+
| Status (`structural` / `promoted`) | a `status::<value>` tag |
|
|
108
|
+
| Owning host, bucket kind | `host`, `kind` |
|
|
109
|
+
| Repo / branch / commit / PR | the `origin_*` fields |
|
|
110
|
+
| What triggered the write | `trigger` |
|
|
111
|
+
|
|
112
|
+
A hidden block does not just look untidy — it is *wrong*. A `seen_count=1` baked
|
|
113
|
+
into prose is stale the first time the lesson recurs, while the column that
|
|
114
|
+
governs promotion moves without it, and a markdown reader is shown a lesson that
|
|
115
|
+
begins mid-sentence. Structure the prose instead: a takeaway title, a visible
|
|
116
|
+
**Applies when** line, then short bold-labelled paragraphs under ~1,500
|
|
117
|
+
characters. The full shape, the field-by-field rationale, and the writing rules
|
|
118
|
+
are [the lesson record](./rules/self-improvement-loops.md#the-lesson-record).
|
|
119
|
+
|
|
96
120
|
## The shared codebase-knowledge layer (automatic cross-loop synergy)
|
|
97
121
|
|
|
98
122
|
A per-host lessons bucket is private to one host. There is also **one shared
|
|
@@ -58,7 +58,7 @@ this rule should say so out loud rather than sell the store.
|
|
|
58
58
|
| | Lesson (`self-improvement-loops.md`) | CI state record (this file) |
|
|
59
59
|
| --- | --- | --- |
|
|
60
60
|
| Author | a model, at the end of a run | a script, deterministically |
|
|
61
|
-
| Value | prose
|
|
61
|
+
| Value | markdown prose only — metadata lives in the store's own fields | JSON (an object, not a bare scalar) |
|
|
62
62
|
| How a reader uses it | **advisory** — a consideration that can be overridden | **authoritative** — parsed and branched on |
|
|
63
63
|
| Recurrence / promotion | yes (`seen_count`, human-gated hardening) | n/a — nothing is inferred, so nothing needs gating |
|
|
64
64
|
| Entrenchment risk | high — the reason those guards exist | none; the risks are cardinality and secrets instead |
|
|
@@ -34,9 +34,9 @@ A lesson is a compile candidate only if **all four** hold:
|
|
|
34
34
|
1. **Trigger detectable without judgement.** The condition that should fire the
|
|
35
35
|
check can be recognized from a file path, a diff shape, or another mechanical
|
|
36
36
|
signal — not "whether this edit is the kind that matters," which needs a
|
|
37
|
-
reader's judgement. This is exactly what the existing "
|
|
38
|
-
concrete" requirement in the lessons loop exists for: a lesson with a vague
|
|
39
|
-
|
|
37
|
+
reader's judgement. This is exactly what the existing "**Applies when** must
|
|
38
|
+
be concrete" requirement in the lessons loop exists for: a lesson with a vague
|
|
39
|
+
applicability signal was never going to compile, so demanding concreteness at
|
|
40
40
|
write time is what keeps the door open later.
|
|
41
41
|
2. **Assertion statable as must-remain-true.** The rule has to be expressible as
|
|
42
42
|
an invariant ("if A changed, B must also be in the changed set"), not a
|
|
@@ -114,8 +114,9 @@ A read-only survey over the memory store. It reuses `dedupe`'s Jaccard clusterin
|
|
|
114
114
|
to find near-duplicate lessons, then ranks the resulting clusters by
|
|
115
115
|
`(summed seen_count × distinct scopes)`, descending. A cluster is worth
|
|
116
116
|
reporting when the summed `seen_count` across its members is at least
|
|
117
|
-
`--min-seen-count` (default 3), or any member
|
|
118
|
-
|
|
117
|
+
`--min-seen-count` (default 3), or any member already declares a non-`active`
|
|
118
|
+
status — read from a `status::<value>` tag, falling back to a legacy `meta`
|
|
119
|
+
comment for lessons written before tags were the convention. For each candidate it prints every memory the merge would
|
|
119
120
|
collapse — deliberately the default view, not hidden behind `--verbose`, because
|
|
120
121
|
that list *is* the point of the command: a human decides from it whether the
|
|
121
122
|
cluster deserves a hand-written entry.
|
|
@@ -127,9 +128,9 @@ you to notice by hand.
|
|
|
127
128
|
|
|
128
129
|
Two things it deliberately does **not** do, on purpose:
|
|
129
130
|
|
|
130
|
-
- **It does not classify
|
|
131
|
-
|
|
132
|
-
glob/command/error-shape. Turning "parses into a detectable trigger" from a
|
|
131
|
+
- **It does not classify applicability signals.** A lesson's raw signal
|
|
132
|
+
(its **Applies when** line, or a legacy `trigger-context` meta field) is
|
|
133
|
+
printed verbatim — never interpreted into a glob/command/error-shape. Turning "parses into a detectable trigger" from a
|
|
133
134
|
string into an actual predicate is the human step the compile pipeline
|
|
134
135
|
protects; automating it would be exactly the "auto-compile" this pipeline
|
|
135
136
|
refuses to do.
|
|
@@ -71,6 +71,11 @@ across runs** — recurrence is the cheap external signal that the lesson is rea
|
|
|
71
71
|
not a one-off. This is the episodic → procedural promotion path, with a human
|
|
72
72
|
gate on the slow tier so a single bad run can never rewrite the host.
|
|
73
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
|
+
|
|
74
79
|
The fast tier is optional: if LoreKit's `memory.*` tools are not connected, the
|
|
75
80
|
loop is a silent no-op (log one line, continue). The slow tier is just editing
|
|
76
81
|
the host and is unaffected.
|
|
@@ -101,24 +106,115 @@ Reserve `branch::` for throwaway notes; a loop normally writes `global` or
|
|
|
101
106
|
### The lesson record
|
|
102
107
|
|
|
103
108
|
A loop lesson is **procedural** ("how to do better next time"), not a fact about
|
|
104
|
-
the user.
|
|
105
|
-
|
|
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:
|
|
106
112
|
|
|
107
113
|
```markdown
|
|
108
|
-
|
|
114
|
+
# <one-line takeaway — what to do, not what the lesson is about>
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
**Applies when:** <concrete signal — file glob, task type, tool name, error shape>
|
|
111
117
|
|
|
112
|
-
**What
|
|
118
|
+
**What happened:** <the concrete observable from the run>
|
|
113
119
|
**Why:** <root cause, if known; "unknown" is allowed>
|
|
114
|
-
**
|
|
120
|
+
**Do this instead:** <prescriptive, actionable, testable instruction>
|
|
115
121
|
**Promotion target:** <the host rule/step this would harden if promoted, or "none">
|
|
116
122
|
```
|
|
117
123
|
|
|
118
|
-
`
|
|
119
|
-
shapes) — never "when it feels relevant" — so the read step can match it
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
122
218
|
|
|
123
219
|
---
|
|
124
220
|
|
|
@@ -135,10 +231,10 @@ memory.search { q: "<keywords>", scopes: ["repo::{owner}/*", "global"], limit: 1
|
|
|
135
231
|
|
|
136
232
|
Then:
|
|
137
233
|
|
|
138
|
-
1. Match each lesson's
|
|
139
|
-
matches.
|
|
140
|
-
|
|
141
|
-
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
|
|
142
238
|
command — it biases the run unless it conflicts with the user's stated intent
|
|
143
239
|
or a task-specific constraint. On conflict, the user's intent wins; surface it.
|
|
144
240
|
3. On a `repo::` vs `global` collision, the `repo::` lesson wins (closer scope).
|
|
@@ -161,23 +257,31 @@ caught something, a near-miss, a guess that paid off. Not on smooth successes.
|
|
|
161
257
|
memory.search { q: "<key words of the lesson>", scopes: ["repo::{owner}/{repo}", "global"], limit: 10 }
|
|
162
258
|
```
|
|
163
259
|
|
|
164
|
-
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`:
|
|
165
262
|
|
|
166
263
|
```text
|
|
167
264
|
memory.write {
|
|
168
|
-
scope:
|
|
169
|
-
key:
|
|
170
|
-
value:
|
|
171
|
-
tags:
|
|
172
|
-
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
|
|
173
271
|
}
|
|
174
272
|
```
|
|
175
273
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
181
285
|
|
|
182
286
|
The privacy pre-flight is never skipped, autonomous or not: a candidate lesson
|
|
183
287
|
containing a secret, token, credential, or PII is **dropped, not written**. The
|
|
@@ -381,8 +485,9 @@ hotspots during review), and every code-changing host — `aw`, `implement-sugge
|
|
|
381
485
|
|
|
382
486
|
After a read or write, a lesson is **promotion-eligible** when either:
|
|
383
487
|
|
|
384
|
-
- `seen_count >= 3` — the same failure recurred across at least three runs
|
|
385
|
-
|
|
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
|
|
386
491
|
one-off.
|
|
387
492
|
|
|
388
493
|
For an eligible lesson, **surface a one-line suggestion — never act silently**:
|
|
@@ -394,8 +499,8 @@ The promotion target follows the lesson's scope: a `global` lesson hardens the
|
|
|
394
499
|
**host's own source** (every user of the host benefits); a `repo::` lesson
|
|
395
500
|
hardens the **repo's own rules / docs** (every teammate in that repo benefits).
|
|
396
501
|
Promotion is a normal, human-reviewed edit — LoreKit does not apply it. After a
|
|
397
|
-
successful promotion, write an UPDATE
|
|
398
|
-
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.
|
|
399
504
|
|
|
400
505
|
A recurring lesson can be promoted a second time, past the prose rule above,
|
|
401
506
|
into a **compiled invariant** — a declarative, mechanically-checked assertion
|
|
@@ -420,9 +525,10 @@ guards are what make the loop safe:
|
|
|
420
525
|
run; it can never silently disable a gate, skip a step, or change a limit. The
|
|
421
526
|
only path from a lesson to changed behavior is the human-reviewed slow tier.
|
|
422
527
|
2. **Recurrence gates promotion, not a single run** (`seen_count >= 3`, or an
|
|
423
|
-
explicit `status
|
|
424
|
-
3. **Every lesson expires**
|
|
425
|
-
|
|
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.
|
|
426
532
|
4. **Contradiction is surfaced, not silently overwritten** — the dedup search
|
|
427
533
|
finds the prior lesson; a genuine reversal is a reviewed decision.
|
|
428
534
|
5. **The privacy pre-flight is never bypassed** — secrets / PII are dropped, not
|
|
@@ -436,13 +542,14 @@ To add a loop to a host called `<host>`:
|
|
|
436
542
|
|
|
437
543
|
- [ ] Pick the bucket: tag `loop::<host>-lessons`, key `<host>-lessons::<slug>`.
|
|
438
544
|
- [ ] Add the **read step** at the start of the host's run (narrow-to-broad
|
|
439
|
-
`memory.list` filtered by the tag; apply matches as considerations
|
|
440
|
-
expired).
|
|
545
|
+
`memory.list` filtered by the tag; apply matches as considerations).
|
|
441
546
|
- [ ] Add the **write step** at the host's existing failure / end-of-run points
|
|
442
|
-
(classify scope, `memory.search` to dedup, `memory.write`).
|
|
443
|
-
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.
|
|
444
551
|
- [ ] Add the **promotion suggestion** when a read/written lesson hits
|
|
445
|
-
`seen_count >= 3` or `status
|
|
552
|
+
`seen_count >= 3` or carries `status::structural`.
|
|
446
553
|
- [ ] State the **entrenchment guards** so a future maintainer does not "optimize
|
|
447
554
|
them away".
|
|
448
555
|
- [ ] Confirm the loop **degrades silently** when `memory.*` is not connected.
|
|
@@ -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
|
}
|