@lorekit/cli 1.31.0 → 1.32.1
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/bin/lorekit.mjs +1 -1
- package/package.json +1 -1
- package/skill/lorekit-setup/rules/self-improvement-loops.md +65 -0
- package/src/lessons-view.mjs +47 -6
- package/src/list.mjs +7 -2
- package/src/store/remote.mjs +6 -2
- package/src/write.mjs +6 -0
package/bin/lorekit.mjs
CHANGED
|
@@ -600,7 +600,7 @@ const KNOWN_FLAGS = [
|
|
|
600
600
|
'dir', 'project', 'global', 'endpoint', 'token', 'mode', 'store',
|
|
601
601
|
'from', 'to', 'apply', 'yes', 'hooks', 'no-hooks', 'force', 'deep', 'adapter',
|
|
602
602
|
'event', 'json', 'scope', 'threshold', 'help', 'version', 'telemetry',
|
|
603
|
-
'value', 'tags', 'source-agent', 'trigger', 'ttl-days', 'clear-ttl', 'org', 'remote', 'local',
|
|
603
|
+
'value', 'tags', 'source-agent', 'trigger', 'kind', 'host', 'ttl-days', 'clear-ttl', 'org', 'remote', 'local',
|
|
604
604
|
'link', 'base', 'q', 'owner', 'range', 'view', 'archived',
|
|
605
605
|
'origin-repo', 'origin-branch', 'origin-commit', 'origin-pr', 'no-origin',
|
|
606
606
|
];
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ The design has two tiers connected by a recurrence gate. Both run on LoreKit.
|
|
|
20
20
|
- [Conventions](#conventions)
|
|
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
|
+
- [The reconcile-on-re-run flow (resolve + record)](#the-reconcile-on-re-run-flow-resolve--record)
|
|
23
24
|
- [Promotion (fast → slow)](#promotion-fast--slow)
|
|
24
25
|
- [Entrenchment guards (do not skip these)](#entrenchment-guards-do-not-skip-these)
|
|
25
26
|
- [Wiring checklist](#wiring-checklist)
|
|
@@ -178,6 +179,70 @@ bar is stricter for `repo::` writes — a repo scope is team-visible.
|
|
|
178
179
|
|
|
179
180
|
---
|
|
180
181
|
|
|
182
|
+
## The reconcile-on-re-run flow (resolve + record)
|
|
183
|
+
|
|
184
|
+
Some hosts do not just fail-and-learn — they **produce durable outputs at a
|
|
185
|
+
shared target that they revisit on later runs**: a reviewer posts comment threads
|
|
186
|
+
on a PR it re-reviews on every push, a triager files issues it re-scans, a linter
|
|
187
|
+
opens tickets it re-opens. For these, a plain read/write loop is not enough:
|
|
188
|
+
stale outputs pile up at the target, and the signal about which outputs were
|
|
189
|
+
*useful* is thrown away.
|
|
190
|
+
|
|
191
|
+
The reconcile-on-re-run flow closes both gaps. On each re-run over the same
|
|
192
|
+
target, the host **reconciles its own prior outputs** in three steps:
|
|
193
|
+
|
|
194
|
+
1. **Classify** each prior output the host itself produced against the current
|
|
195
|
+
state of the target. The three outcomes that carry signal:
|
|
196
|
+
|
|
197
|
+
| Outcome | Meaning | Evidence |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| **resolved** | The output was acted on — the thing it flagged is now handled | the flagged region changed and the finding no longer reproduces, or the owner acknowledged it |
|
|
200
|
+
| **declined** | The owner explicitly rejected it | a "won't fix" / "by design" reply, a 👎 |
|
|
201
|
+
| **still-open** | The finding still reproduces this run | the host re-produces the same output |
|
|
202
|
+
|
|
203
|
+
2. **Clean up at the target.** For `resolved` and `declined` outputs, close them
|
|
204
|
+
at the source — resolve the thread, close the ticket, check the box — so a
|
|
205
|
+
re-run leaves the target tidier than it found it instead of accumulating
|
|
206
|
+
cruft. **Never** close a `still-open` output; that would hide a live finding.
|
|
207
|
+
Only ever touch outputs the host itself authored.
|
|
208
|
+
|
|
209
|
+
3. **Record the outcome** to a **Signal**-shaped bucket (a durable, per-target
|
|
210
|
+
relevance memory — distinct from the fast lessons bucket). Write `resolved` as
|
|
211
|
+
a positive signal for that output's *pattern* and `declined` as a negative
|
|
212
|
+
one, keyed by a stable pattern fingerprint (never by a line number or an id
|
|
213
|
+
that drifts). Over runs this bucket teaches the host which of its output
|
|
214
|
+
patterns get acted on in this target and which are noise — read it at the
|
|
215
|
+
start of a run to suppress the reliably-declined patterns and reinforce the
|
|
216
|
+
reliably-resolved ones. `still-open` writes nothing: there is no outcome yet.
|
|
217
|
+
|
|
218
|
+
The Signal bucket is a second bucket alongside the lessons one, in the same
|
|
219
|
+
grammar as [Conventions](#conventions):
|
|
220
|
+
|
|
221
|
+
- **Tag:** `loop::<host>-<signal>` — e.g. `loop::reviewer-comment-relevance`.
|
|
222
|
+
Reads filter by it; writes always carry it.
|
|
223
|
+
- **Key:** `<host>-<signal>::<pattern-fingerprint>` — e.g.
|
|
224
|
+
`reviewer-comment-relevance::unsupported-cross-repo-claim`. The fingerprint
|
|
225
|
+
is the key segment, so the same `scope` + `key` overwrites in place and one
|
|
226
|
+
output pattern accumulates one record across runs.
|
|
227
|
+
|
|
228
|
+
Two guards keep this honest, both instances of the entrenchment guards below:
|
|
229
|
+
|
|
230
|
+
- **Absence of confirmation is not resolution.** If a re-run did not re-scan the
|
|
231
|
+
region a prior output covers (e.g. it only looked at the diff), the output is
|
|
232
|
+
`still-open`, not `resolved` — silence is not a fix.
|
|
233
|
+
- **The cleanup is idempotent and non-fatal.** A target already closed is
|
|
234
|
+
skipped; a cleanup error is logged and never fails the run.
|
|
235
|
+
|
|
236
|
+
Wire it as its own step at the host's re-run seam, gated on "a prior run's output
|
|
237
|
+
exists at this target". It composes with the read/write steps: the Signal bucket
|
|
238
|
+
it writes is read back at the next run's read step. The reference implementation
|
|
239
|
+
is the `agent-skills` `pr-reviewer` agent: it resolves its own addressed PR
|
|
240
|
+
threads on each commit-triggered re-review and records the fixed/declined
|
|
241
|
+
outcome to a `reviewer-comment-relevance` bucket, whose classification and
|
|
242
|
+
record shape are specified in `agents/shared/rules/comment-relevance-memory.md`.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
181
246
|
## Promotion (fast → slow)
|
|
182
247
|
|
|
183
248
|
After a read or write, a lesson is **promotion-eligible** when either:
|
package/src/lessons-view.mjs
CHANGED
|
@@ -37,16 +37,39 @@ export function scopeList({ projectScope, branchScope, repoScope } = {}) {
|
|
|
37
37
|
return [...new Set([projectScope, branchScope, repoScope, 'global'].filter(Boolean))];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Infer { kind, host } from a memory's loop tags — the CLI-local mirror of
|
|
41
|
+
// `@lorekit/schemas` `inferKindHost` (the CLI has no schemas dependency). Kept
|
|
42
|
+
// deliberately small and in lockstep with that source, including the 64-char
|
|
43
|
+
// host clamp. Lets the offline store, whose rows carry no kind/host column,
|
|
44
|
+
// still be filtered and badged by taxonomy from the tags it does store.
|
|
45
|
+
function inferKindHostFromTags(tags) {
|
|
46
|
+
if (!Array.isArray(tags)) return {};
|
|
47
|
+
for (const tag of tags) {
|
|
48
|
+
if (tag === 'loop::review-outcomes') return { kind: 'bus', host: 'review' };
|
|
49
|
+
if (tag === 'loop::reviewer-comment-relevance') return { kind: 'signal', host: 'reviewer' };
|
|
50
|
+
const m = typeof tag === 'string' ? /^loop::(.+)-lessons$/.exec(tag) : null;
|
|
51
|
+
if (m && m[1] && m[1].length <= 64) return { kind: 'lesson', host: m[1] };
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
// Normalize an entry from either store (local markdown row or hosted DB row)
|
|
41
57
|
// into one stable shape the view + `--json` output can rely on. Remote rows may
|
|
42
|
-
// spell the timestamp `updated_at`; local rows use `updated`.
|
|
58
|
+
// spell the timestamp `updated_at`; local rows use `updated`. When a row carries
|
|
59
|
+
// no explicit kind/host (every offline row, and any remote row written before
|
|
60
|
+
// migration 00056), fall back to the taxonomy inferred from its tags so the
|
|
61
|
+
// badge and the `--kind`/`--host` filter behave the same in both sections.
|
|
43
62
|
export function normalizeEntry(e = {}) {
|
|
63
|
+
const tags = Array.isArray(e.tags) ? e.tags : [];
|
|
64
|
+
const inferred = inferKindHostFromTags(tags);
|
|
44
65
|
return {
|
|
45
66
|
scope: e.scope ?? null,
|
|
46
67
|
key: e.key ?? null,
|
|
47
68
|
value: e.value == null ? '' : String(e.value),
|
|
48
69
|
updated: e.updated ?? e.updated_at ?? null,
|
|
49
|
-
tags
|
|
70
|
+
tags,
|
|
71
|
+
kind: e.kind ?? inferred.kind ?? null,
|
|
72
|
+
host: e.host ?? inferred.host ?? null,
|
|
50
73
|
};
|
|
51
74
|
}
|
|
52
75
|
|
|
@@ -407,13 +430,27 @@ export function clusterDuplicates(entries = [], threshold = 0.8) {
|
|
|
407
430
|
// `store.list({scope})` contract. Returns ordered per-scope groups plus a total
|
|
408
431
|
// — a per-scope read failure is captured on the group, never thrown, so one bad
|
|
409
432
|
// scope can't abort the listing. `store` may be a local or remote store.
|
|
410
|
-
export async function gather(store, scopes) {
|
|
433
|
+
export async function gather(store, scopes, filters = {}) {
|
|
434
|
+
// Parse the taxonomy filters into value sets. `filters` is passed to the store
|
|
435
|
+
// too (the remote narrows server-side); we ALSO post-filter the normalized
|
|
436
|
+
// entries here so the offline store — which ignores kind/host in its own
|
|
437
|
+
// `list()` — stays consistent with remote. Post-filtering the remote rows is
|
|
438
|
+
// idempotent (they were already narrowed) and matches on the same inferred
|
|
439
|
+
// taxonomy a row without explicit columns gets in normalizeEntry.
|
|
440
|
+
const wanted = (v) =>
|
|
441
|
+
v == null ? null : new Set(String(v).split(',').map((s) => s.trim()).filter(Boolean));
|
|
442
|
+
const kindSet = wanted(filters.kind);
|
|
443
|
+
const hostSet = wanted(filters.host);
|
|
444
|
+
const keep = (e) =>
|
|
445
|
+
(!kindSet || (e.kind != null && kindSet.has(e.kind))) &&
|
|
446
|
+
(!hostSet || (e.host != null && hostSet.has(e.host)));
|
|
447
|
+
|
|
411
448
|
const groups = [];
|
|
412
449
|
let total = 0;
|
|
413
450
|
for (const scope of scopes) {
|
|
414
451
|
let res;
|
|
415
452
|
try {
|
|
416
|
-
res = await store.list({ scope });
|
|
453
|
+
res = await store.list({ scope, ...filters });
|
|
417
454
|
} catch (e) {
|
|
418
455
|
res = { ok: false, networkError: (e && e.message) || 'error' };
|
|
419
456
|
}
|
|
@@ -421,7 +458,7 @@ export async function gather(store, scopes) {
|
|
|
421
458
|
groups.push({ scope, entries: [], error: describeError(res) });
|
|
422
459
|
continue;
|
|
423
460
|
}
|
|
424
|
-
const entries = (res.entries || []).map(normalizeEntry);
|
|
461
|
+
const entries = (res.entries || []).map(normalizeEntry).filter(keep);
|
|
425
462
|
total += entries.length;
|
|
426
463
|
groups.push({ scope, entries, error: null });
|
|
427
464
|
}
|
|
@@ -456,7 +493,11 @@ export function renderSection(header, section) {
|
|
|
456
493
|
}
|
|
457
494
|
for (const e of g.entries) {
|
|
458
495
|
const when = e.updated ? ` ${c.dim(`(updated ${shortDate(e.updated)})`)}` : '';
|
|
459
|
-
|
|
496
|
+
// Taxonomy badge — the kind (and host, when known) so the three families
|
|
497
|
+
// are visible at a glance in the list. Omitted for rows written before the
|
|
498
|
+
// taxonomy existed (NULL kind).
|
|
499
|
+
const badge = e.kind ? ` ${c.dim(`[${e.kind}${e.host ? `·${e.host}` : ''}]`)}` : '';
|
|
500
|
+
log(` ${c.cyan('•')} ${g.scope}::${e.key}${badge}${when}`);
|
|
460
501
|
if (e.value) log(` ${c.dim(preview(e.value))}`);
|
|
461
502
|
}
|
|
462
503
|
}
|
package/src/list.mjs
CHANGED
|
@@ -41,6 +41,11 @@ export async function list(args) {
|
|
|
41
41
|
// Default to every applicable scope; `--scope <s>` narrows to one (an explicit
|
|
42
42
|
// scope outside the applicable set is honoured — the user asked for it).
|
|
43
43
|
const scopes = args.scope && typeof args.scope === 'string' ? [args.scope] : scopeList(scopeInfo);
|
|
44
|
+
// Optional taxonomy filters — `--kind lesson --host reviewer` narrows to one
|
|
45
|
+
// family/owner. Comma lists are honoured by the remote store's query builder.
|
|
46
|
+
const filters = {};
|
|
47
|
+
if (typeof args.kind === 'string') filters.kind = args.kind;
|
|
48
|
+
if (typeof args.host === 'string') filters.host = args.host;
|
|
44
49
|
|
|
45
50
|
// `--link` short-circuits: print the Explorer deep link for the current
|
|
46
51
|
// context (the most-specific applicable scope, or `--scope`), no store reads.
|
|
@@ -64,9 +69,9 @@ export async function list(args) {
|
|
|
64
69
|
// agent-facing control model enforces, honored here in the human read view.
|
|
65
70
|
const { localDenied, remoteDenied } = resolveDenies(root, { env });
|
|
66
71
|
|
|
67
|
-
const offline = localDenied ? { groups: [], total: 0 } : await gather(local, scopes);
|
|
72
|
+
const offline = localDenied ? { groups: [], total: 0 } : await gather(local, scopes, filters);
|
|
68
73
|
const remoteAvailable = !remoteDenied && remote.usable();
|
|
69
|
-
const remoteResult = remoteAvailable ? await gather(remote, scopes) : { groups: [], total: 0 };
|
|
74
|
+
const remoteResult = remoteAvailable ? await gather(remote, scopes, filters) : { groups: [], total: 0 };
|
|
70
75
|
|
|
71
76
|
const offlineSection = localDenied
|
|
72
77
|
? { available: false, reason: `disabled by deny constraint (${localDenied.source})` }
|
package/src/store/remote.mjs
CHANGED
|
@@ -58,10 +58,12 @@ class RemoteStore {
|
|
|
58
58
|
|
|
59
59
|
// ── Memory operations → REST ──────────────────────────────────────────────
|
|
60
60
|
|
|
61
|
-
async list({ scope, tags, limit } = {}) {
|
|
61
|
+
async list({ scope, tags, kind, host, limit } = {}) {
|
|
62
62
|
const p = new URLSearchParams();
|
|
63
63
|
if (scope) p.set('scope', scope);
|
|
64
64
|
if (tags?.length) p.set('tags', Array.isArray(tags) ? tags.join(',') : tags);
|
|
65
|
+
if (kind) p.set('kind', Array.isArray(kind) ? kind.join(',') : kind);
|
|
66
|
+
if (host) p.set('host', Array.isArray(host) ? host.join(',') : host);
|
|
65
67
|
if (limit) p.set('limit', String(limit));
|
|
66
68
|
const res = await this._rest(`/memories?${p}`);
|
|
67
69
|
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
@@ -92,13 +94,15 @@ class RemoteStore {
|
|
|
92
94
|
|
|
93
95
|
async write(args = {}) {
|
|
94
96
|
const {
|
|
95
|
-
scope, key, value, tags, source_agent, trigger, org, ttl_days, clear_ttl, created_at,
|
|
97
|
+
scope, key, value, tags, source_agent, trigger, kind, host, org, ttl_days, clear_ttl, created_at,
|
|
96
98
|
origin_repo, origin_branch, origin_commit, origin_pr,
|
|
97
99
|
} = args;
|
|
98
100
|
const body = { scope, key, value };
|
|
99
101
|
if (tags !== undefined) body.tags = tags;
|
|
100
102
|
if (source_agent !== undefined) body.source_agent = source_agent;
|
|
101
103
|
if (trigger !== undefined) body.trigger = trigger;
|
|
104
|
+
if (kind !== undefined) body.kind = kind;
|
|
105
|
+
if (host !== undefined) body.host = host;
|
|
102
106
|
if (org !== undefined) body.org = org;
|
|
103
107
|
if (ttl_days !== undefined) body.ttl_days = ttl_days;
|
|
104
108
|
if (clear_ttl !== undefined) body.clear_ttl = clear_ttl;
|
package/src/write.mjs
CHANGED
|
@@ -120,6 +120,10 @@ export async function write(args) {
|
|
|
120
120
|
const tags = args.tags ? String(args.tags).split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
121
121
|
const sourceAgent = typeof args['source-agent'] === 'string' ? args['source-agent'] : undefined;
|
|
122
122
|
const trigger = typeof args.trigger === 'string' ? args.trigger : undefined;
|
|
123
|
+
// Taxonomy overrides. Omitted → the server infers kind/host from a
|
|
124
|
+
// `loop::<host>-lessons` tag, so a tagged write needs neither flag.
|
|
125
|
+
const kind = typeof args.kind === 'string' ? args.kind : undefined;
|
|
126
|
+
const host = typeof args.host === 'string' ? args.host : undefined;
|
|
123
127
|
// `--ttl-days` is validated HERE, at the flag seam, rather than being left to the
|
|
124
128
|
// store: a truthiness test silently swallowed `--ttl-days 0` (falsy) and
|
|
125
129
|
// `--ttl-days abc` (NaN, dropped again by the `ttl_days` spread further down), so
|
|
@@ -258,6 +262,8 @@ export async function write(args) {
|
|
|
258
262
|
...(tags.length ? { tags } : {}),
|
|
259
263
|
...(sourceAgent ? { source_agent: sourceAgent } : {}),
|
|
260
264
|
...(trigger ? { trigger } : {}),
|
|
265
|
+
...(kind ? { kind } : {}),
|
|
266
|
+
...(host ? { host } : {}),
|
|
261
267
|
...(ttlDays ? { ttl_days: ttlDays } : {}),
|
|
262
268
|
...(clearTtl ? { clear_ttl: true } : {}),
|
|
263
269
|
...(orgSlug ? { org: orgSlug } : {}),
|