@lorekit/cli 1.69.0 → 1.70.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.
@@ -0,0 +1,86 @@
1
+ # Recipe card — reviewer / reconcile host
2
+
3
+ For a host that **produces durable outputs at a shared target it revisits**: a PR
4
+ reviewer posting comment threads it re-reviews on every push, a triager filing issues
5
+ it re-scans, a linter opening tickets. A plain read/write loop is not enough — stale
6
+ outputs pile up at the target and the signal about which outputs were *useful* is
7
+ thrown away. This card adds the **reconcile-on-re-run** flow on top of the lessons
8
+ loop, feeding a second **Signal** bucket.
9
+
10
+ Fill in: `<host>` (e.g. `reviewer`), `<signal>` (e.g. `comment-relevance`),
11
+ `<owner>/<repo>`. `N = 5`.
12
+
13
+ ## Buckets (two)
14
+
15
+ - Lessons: tag `loop::<host>-lessons`, key `<host>-lessons::<slug>` — how to review better.
16
+ - Signal: tag `loop::<host>-<signal>`, key `<host>-<signal>::<pattern-fingerprint>` —
17
+ which of the host's OUTPUT PATTERNS get acted on vs declined at this target.
18
+
19
+ ## Read step — start of run
20
+
21
+ ```text
22
+ # Own lessons (capped at N per scope):
23
+ memory.list { scope: "repo::<owner>/<repo>", tags: ["loop::<host>-lessons"], limit: 5 }
24
+ memory.list { scope: "global", tags: ["loop::<host>-lessons"], limit: 5 }
25
+
26
+ # Signal bucket — suppress reliably-declined patterns, reinforce reliably-resolved ones:
27
+ memory.list { scope: "repo::<owner>/<repo>", tags: ["loop::<host>-<signal>"], limit: 20 }
28
+ ```
29
+
30
+ ## Reconcile step — at the re-run seam, gated on "a prior output exists at this target"
31
+
32
+ For each prior output the host itself authored, classify against the current target:
33
+
34
+ | Outcome | Meaning | Evidence |
35
+ | --- | --- | --- |
36
+ | **resolved** | acted on — the flagged thing is handled | the region changed and the finding no longer reproduces, or the owner acknowledged it |
37
+ | **declined** | explicitly rejected | a "won't fix" / "by design" reply, a 👎 |
38
+ | **still-open** | still reproduces this run | the host re-produces the same output |
39
+
40
+ Then:
41
+
42
+ 1. **Clean up** `resolved` + `declined` at the source (resolve the thread, close the
43
+ ticket). **Never** touch a `still-open` output. Only ever touch outputs the host
44
+ authored. Cleanup is idempotent and non-fatal (a cleanup error is logged, never
45
+ fails the run).
46
+ 2. **Record the outcome** to the Signal bucket, keyed by a stable pattern fingerprint
47
+ (never a line number or a drifting id):
48
+
49
+ ```text
50
+ memory.write {
51
+ scope: "repo::<owner>/<repo>",
52
+ key: "<host>-<signal>::<pattern-fingerprint>",
53
+ value: "<markdown: the pattern, and resolved-vs-declined evidence — no hidden blocks>",
54
+ tags: ["loop::<host>-<signal>", "status::<resolved | declined>"],
55
+ trigger: "reconcile",
56
+ ttl_days: 90
57
+ }
58
+ ```
59
+
60
+ `still-open` writes nothing — there is no outcome yet. **Absence of confirmation is not
61
+ resolution**: if a re-run did not re-scan the region a prior output covers, it is
62
+ `still-open`, not `resolved`.
63
+
64
+ ## Write step (lessons) — on friction, as usual
65
+
66
+ Same as any lessons loop — see [code-changing-agent.md](./code-changing-agent.md#write-step--on-failure--at-end-of-run)
67
+ for the write shape.
68
+
69
+ ## Fire-once check
70
+
71
+ 1. Produce an output at a test target, then resolve it at the source by hand.
72
+ 2. Re-run; confirm the host classifies it `resolved`, cleans it up, and writes a
73
+ positive Signal record for its pattern.
74
+ 3. Start a third run; confirm the Signal record surfaces and reinforces that pattern.
75
+
76
+ ## Reference implementation
77
+
78
+ The `agent-skills` `pr-reviewer` agent: it resolves its own addressed PR threads on
79
+ each commit-triggered re-review and records the fixed/declined outcome to a
80
+ `reviewer-comment-relevance` bucket. Full flow:
81
+ [the reconcile-on-re-run flow](../rules/self-improvement-loops.md#the-reconcile-on-re-run-flow-resolve--record).
82
+
83
+ ## Prove it
84
+
85
+ [proving-improvement.md](../rules/proving-improvement.md) — for this host the signal is
86
+ the **decline rate** of the host's outputs trending down over runs.
@@ -1,8 +1,10 @@
1
1
  // `lorekit lint` — flag low-quality lessons across the applicable scopes and
2
2
  // both stores. Each finding names the rule it violated (empty/whitespace value,
3
3
  // suspiciously short value, untrimmed value, empty key, volatile key, malformed
4
- // scope). The rules are pure predicates in `lessons-view.mjs` (`LINT_RULES` /
5
- // `lintEntry`), each independently unit-tested.
4
+ // scope, unkinded state record, hidden metadata — an HTML comment / front-matter /
5
+ // `key=value` header in a body that should be pure markdown). The rules are pure
6
+ // predicates in `lessons-view.mjs` (`LINT_RULES` / `lintEntry`), each
7
+ // independently unit-tested.
6
8
  //
7
9
  // Exit convention: `lint` exits NON-ZERO (1) when any finding exists, so it is
8
10
  // usable as a CI gate (`lorekit lint || fail`); a clean run — or a run where the
@@ -253,6 +253,36 @@ export function diffGroups(offline = {}, remote = {}) {
253
253
  // too terse to carry a durable observation (e.g. "yes", "fixed", "todo").
254
254
  export const MIN_VALUE_LEN = 12;
255
255
 
256
+ // Blank the INTERIOR of fenced code blocks (``` or ~~~ fences) so example content
257
+ // — a lesson documenting an `<!-- MARKER -->` or pasting a ```yaml front-matter
258
+ // sample — is never mistaken for hidden metadata. Such content renders as VISIBLE
259
+ // fenced text, the opposite of the digest-hidden block `hidden-metadata` targets,
260
+ // and `lint` is a CI gate, so a false positive there fails a legitimate lesson.
261
+ // Line positions are preserved (interiors and fence lines become empty) so the
262
+ // heading-anchored checks still see the real document structure. A closing fence
263
+ // is ≥3 of the SAME character as the opener with no trailing info string
264
+ // (CommonMark); an opener may carry an info string (```yaml). Pure.
265
+ function stripFencedCode(value) {
266
+ let fence = null; // the active fence character (` or ~), or null when outside a block
267
+ return String(value)
268
+ .split('\n')
269
+ .map((line) => {
270
+ const m = line.match(/^\s*(`{3,}|~{3,})/);
271
+ if (fence === null) {
272
+ if (m) {
273
+ fence = m[1][0];
274
+ return '';
275
+ }
276
+ return line;
277
+ }
278
+ if (m && m[1][0] === fence && line.trim().replace(/[`~\s]/g, '') === '') {
279
+ fence = null;
280
+ }
281
+ return '';
282
+ })
283
+ .join('\n');
284
+ }
285
+
256
286
  // The lint rule set: each a pure predicate over a normalized entry returning a
257
287
  // short reason string when it FIRES, or null when the entry is clean. Kept as
258
288
  // discrete named functions so each rule is independently unit-testable and the
@@ -335,6 +365,47 @@ export const LINT_RULES = {
335
365
  if (parsed === null || typeof parsed !== 'object') return null; // a bare scalar — short-value's to catch when short, otherwise unjudged.
336
366
  return 'value is a JSON object/array with no kind set — it renders as a raw JSON blob in every SessionStart digest; set --kind bus or --kind signal';
337
367
  },
368
+ // A lesson body is markdown for humans — no HTML comment, no front-matter, no
369
+ // `key=value` header. The `<!-- meta: seen_count=… status=… trigger-context=… -->`
370
+ // block is a REPUDIATED legacy convention this repo's lorekit-setup skill once
371
+ // prescribed (still parsed as a read-fallback in `candidates-pure.mjs` /
372
+ // `commands/invariants.mjs`, and skipped by the digest preview in `core/lessons.mjs`).
373
+ // It is wrong on the merits: an HTML comment renders to nothing, so a human sees a
374
+ // lesson starting mid-sentence, while a baked-in `seen_count`/`status`/`trigger`
375
+ // silently disagrees with the store's own column/tag/field. Conservative like the
376
+ // other rules — three concrete shapes only, never a fuzzy "looks like metadata":
377
+ // 1. any HTML comment (`<!--`), the strongest and most common offender;
378
+ // 2. a body that OPENS with a YAML front-matter block (`---` as the first line);
379
+ // 3. a machine-metadata header (`meta:`, `seen_count`, `status=`, `expires`,
380
+ // `ttl(_days)`, `trigger-context`) appearing BEFORE the first `#` title, so a
381
+ // legitimate prose line mid-body never trips it.
382
+ // All three run against a fence-stripped copy (`stripFencedCode`) so a lesson that
383
+ // DOCUMENTS one of these shapes inside a code block is never flagged.
384
+ 'hidden-metadata': (e) => {
385
+ const raw = String(e.value ?? '');
386
+ if (!raw.trim()) return null; // an empty value is `empty-value`'s to report.
387
+ const v = stripFencedCode(raw);
388
+ if (!v.trim()) return null; // nothing outside code fences — no prose metadata to flag.
389
+ if (v.includes('<!--')) {
390
+ return "value contains an HTML comment — lesson bodies are pure markdown; move seen_count → the column, status → a status:: tag, trigger → the trigger field";
391
+ }
392
+ const lines = v.split('\n');
393
+ const firstNonEmpty = lines.find((l) => l.trim() !== '');
394
+ if (firstNonEmpty !== undefined && firstNonEmpty.trim() === '---') {
395
+ return 'value opens with a front-matter block — lesson bodies carry no front-matter; every stored fact belongs in its own write field';
396
+ }
397
+ const headingIdx = lines.findIndex((l) => /^\s*#/.test(l));
398
+ if (headingIdx > 0) {
399
+ const metaHeader = lines
400
+ .slice(0, headingIdx)
401
+ .filter((l) => l.trim() !== '')
402
+ .find((l) => /^\s*(meta\b|seen_count\b|status\s*=|expires\b|ttl(?:_days)?\b|trigger[-_]context\b)/i.test(l));
403
+ if (metaHeader) {
404
+ return `value has a machine-metadata header before the title ('${metaHeader.trim().slice(0, 40)}') — move it to the store's own fields`;
405
+ }
406
+ }
407
+ return null;
408
+ },
338
409
  };
339
410
 
340
411
  // Run every lint rule against one normalized entry, returning the findings it