@lorekit/cli 1.32.0 → 1.33.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/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/core/lessons.mjs
CHANGED
|
@@ -140,6 +140,12 @@ export function failureQuery(toolName, toolResponse) {
|
|
|
140
140
|
// `cap`. Pure and best-effort: no terms or no lessons → empty (the caller then
|
|
141
141
|
// falls back to the write-nudge alone). Preserves `lessons` order, so the
|
|
142
142
|
// most-specific scope's relevant lesson surfaces first.
|
|
143
|
+
//
|
|
144
|
+
// This is the LITERAL, client-side matcher — it can only ever narrow a list it
|
|
145
|
+
// is handed. On the failure hot path it has been superseded by
|
|
146
|
+
// `relevantLessonsFromStore`, which QUERIES the store instead of post-filtering
|
|
147
|
+
// the already-injected set (see that function's note). Kept for its unit tests
|
|
148
|
+
// and as the pure building block behind the store-delegated ranking.
|
|
143
149
|
export function relevantLessons(lessons, terms, cap = MAX_RELEVANT) {
|
|
144
150
|
if (!Array.isArray(lessons) || !lessons.length || !Array.isArray(terms) || !terms.length) {
|
|
145
151
|
return [];
|
|
@@ -152,6 +158,79 @@ export function relevantLessons(lessons, terms, cap = MAX_RELEVANT) {
|
|
|
152
158
|
return out;
|
|
153
159
|
}
|
|
154
160
|
|
|
161
|
+
// Rank the store-search hits for a failure lookup. `resultsPerTerm` is one entry
|
|
162
|
+
// list per failure term (a term that errored or matched nothing contributes an
|
|
163
|
+
// empty list). A lesson that matched MORE terms is more relevant, so it ranks
|
|
164
|
+
// higher; ties break toward the MORE-SPECIFIC scope (its position in
|
|
165
|
+
// `readOrder`, which is most-specific-first) and then toward first-seen order
|
|
166
|
+
// for a stable result. De-duplicates by `scope::key` across every term/scope,
|
|
167
|
+
// keeping the first-seen entry. Pure and total — any non-array input degrades to
|
|
168
|
+
// [] rather than throwing (this runs inside the best-effort failure hook).
|
|
169
|
+
// Capped at `cap`.
|
|
170
|
+
export function rankRelevant(resultsPerTerm, readOrder = [], cap = MAX_RELEVANT) {
|
|
171
|
+
if (!Array.isArray(resultsPerTerm)) return [];
|
|
172
|
+
const order = new Map(
|
|
173
|
+
(Array.isArray(readOrder) ? readOrder : []).map((s, i) => [s, i]),
|
|
174
|
+
);
|
|
175
|
+
const UNKNOWN = Number.MAX_SAFE_INTEGER; // a scope not in readOrder sorts last
|
|
176
|
+
const byId = new Map();
|
|
177
|
+
let seq = 0;
|
|
178
|
+
for (const entries of resultsPerTerm) {
|
|
179
|
+
if (!Array.isArray(entries)) continue;
|
|
180
|
+
const seenThisTerm = new Set(); // one term can't count a lesson twice
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
if (!e || !e.key) continue;
|
|
183
|
+
const id = `${e.scope ?? ''}::${e.key}`;
|
|
184
|
+
if (seenThisTerm.has(id)) continue;
|
|
185
|
+
seenThisTerm.add(id);
|
|
186
|
+
const cur = byId.get(id);
|
|
187
|
+
if (cur) {
|
|
188
|
+
cur.matches += 1;
|
|
189
|
+
} else {
|
|
190
|
+
byId.set(id, {
|
|
191
|
+
entry: e,
|
|
192
|
+
matches: 1,
|
|
193
|
+
rank: order.has(e.scope) ? order.get(e.scope) : UNKNOWN,
|
|
194
|
+
seq: seq++,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [...byId.values()]
|
|
200
|
+
.sort((a, b) => b.matches - a.matches || a.rank - b.rank || a.seq - b.seq)
|
|
201
|
+
.slice(0, Math.max(0, cap))
|
|
202
|
+
.map((r) => r.entry);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Retrieve lessons relevant to a tool failure by QUERYING the store across the
|
|
206
|
+
// scope hierarchy — as opposed to `relevantLessons`, which post-filtered the
|
|
207
|
+
// already-injected set and could therefore only ever resurface a lesson that was
|
|
208
|
+
// going to be shown anyway (a lesson in a sibling scope, or one past the
|
|
209
|
+
// per-scope read cap, was unreachable). Runs one search per distilled term
|
|
210
|
+
// (bounded by `failureQuery`'s MAX_TERMS), concurrently and best-effort: a failed
|
|
211
|
+
// or throwing search contributes nothing. Matching is DELEGATED to the store — a
|
|
212
|
+
// literal substring over the full scope for the local store, server-side FTS
|
|
213
|
+
// (with stemming, so `connect` matches `connection`) for the remote — then the
|
|
214
|
+
// union is ranked by the pure `rankRelevant`. Returns [] on any failure so the
|
|
215
|
+
// caller falls back to the write-nudge alone.
|
|
216
|
+
export async function relevantLessonsFromStore(store, scope, terms, { cap = MAX_RELEVANT } = {}) {
|
|
217
|
+
if (!store || typeof store.search !== 'function') return [];
|
|
218
|
+
if (!scope || !Array.isArray(scope.readOrder) || scope.readOrder.length === 0) return [];
|
|
219
|
+
if (!Array.isArray(terms) || terms.length === 0) return [];
|
|
220
|
+
const scopes = scope.readOrder;
|
|
221
|
+
const resultsPerTerm = await Promise.all(
|
|
222
|
+
terms.map(async (t) => {
|
|
223
|
+
try {
|
|
224
|
+
const res = await store.search({ q: t, scopes });
|
|
225
|
+
return res && res.ok && Array.isArray(res.entries) ? res.entries : [];
|
|
226
|
+
} catch {
|
|
227
|
+
return []; // best-effort: one failed term never sinks the lookup
|
|
228
|
+
}
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
return rankRelevant(resultsPerTerm, scopes, cap);
|
|
232
|
+
}
|
|
233
|
+
|
|
155
234
|
// Render the relevant-lessons block injected alongside the failure nudge, or
|
|
156
235
|
// null when nothing matched. Same compact-index shape as `formatLessons`, with a
|
|
157
236
|
// touch more hook per line (there are at most MAX_RELEVANT and they're directly
|
package/src/hook.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
retrospectiveNudge,
|
|
14
14
|
failureNudge,
|
|
15
15
|
failureQuery,
|
|
16
|
-
|
|
16
|
+
relevantLessonsFromStore,
|
|
17
17
|
formatRelevantLessons,
|
|
18
18
|
writeConfirmation,
|
|
19
19
|
} from './core/lessons.mjs';
|
|
@@ -148,9 +148,13 @@ async function run(args) {
|
|
|
148
148
|
try {
|
|
149
149
|
const store = createStore(control);
|
|
150
150
|
if (store) {
|
|
151
|
-
|
|
151
|
+
// QUERY the store across the scope hierarchy for lessons matching this
|
|
152
|
+
// failure — not a post-filter of the SessionStart-injected set, which
|
|
153
|
+
// could only ever resurface an already-shown lesson. Matching is the
|
|
154
|
+
// store's job (server FTS with stemming for remote, full-scope substring
|
|
155
|
+
// for local), so a paraphrased prior lesson can still surface.
|
|
152
156
|
const terms = failureQuery(parsed.toolName, parsed.toolResponse);
|
|
153
|
-
relevant = formatRelevantLessons(
|
|
157
|
+
relevant = formatRelevantLessons(await relevantLessonsFromStore(store, scope, terms));
|
|
154
158
|
}
|
|
155
159
|
} catch {
|
|
156
160
|
relevant = null; // never let a lesson lookup break the failure nudge
|