@olegkoval/agent-skills 1.59.2 → 1.60.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.
Files changed (40) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/.github/prompts/ingest-publication-slice.prompt.md +43 -0
  3. package/.github/prompts/self-critique.prompt.md +167 -43
  4. package/.kiro/steering/ingest-publication-slice.md +42 -0
  5. package/.windsurf/rules/ingest-publication-slice.md +41 -0
  6. package/README.md +5 -3
  7. package/adapters/claude/olko-product/skills/ingest-publication-slice/SKILL.md +44 -0
  8. package/adapters/claude/olko-reflection/skills/self-critique/SKILL.md +167 -43
  9. package/adapters/claude/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  10. package/adapters/claude/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  11. package/adapters/codex/olko-product/README.md +1 -0
  12. package/adapters/cursor/olko-product/skills/ingest-publication-slice/SKILL.md +44 -0
  13. package/adapters/cursor/olko-reflection/skills/self-critique/SKILL.md +167 -43
  14. package/adapters/cursor/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  15. package/adapters/cursor/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  16. package/adapters/grok/olko-product/skills/ingest-publication-slice/SKILL.md +44 -0
  17. package/adapters/grok/olko-reflection/skills/self-critique/SKILL.md +167 -43
  18. package/adapters/grok/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  19. package/adapters/grok/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  20. package/adapters/hermes/olko-product/README.md +1 -0
  21. package/catalog/skills.json +23 -1
  22. package/docs/superpowers/plans/2026-10-27-self-critique-v2.md +306 -0
  23. package/docs/superpowers/specs/2026-10-27-self-critique-v2-spec.md +104 -0
  24. package/package.json +1 -1
  25. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  26. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  27. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  28. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  29. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  30. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  31. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  32. package/plugins/olko-product/skills/ingest-publication-slice/SKILL.md +43 -0
  33. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  34. package/plugins/olko-reflection/skills/self-critique/SKILL.md +167 -43
  35. package/plugins/olko-reflection/skills/self-critique/scripts/claim-ledger.mjs +159 -0
  36. package/plugins/olko-reflection/skills/self-critique/scripts/rule-store.mjs +193 -0
  37. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  38. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  39. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  40. package/scripts/lib/catalog.mjs +1 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: self-critique
3
- description: Adversarially critique your own last substantial answer before the user has to. Spawn a critic agent that verifies every claim against live sources, checks similar and related issues, finds patterns, and addresses comments directly to you. Loop (revise, re-score) until satisfied, report where you were wrong, iterations-to-satisfy, the numeric improvement and a final score, then convert what the critic caught into a durable rule so the next session does not repeat it. Use when the user asks you to criticize, challenge, stress-test, or red-team your own answer, or accepts a "critical-thinking review" offer.
3
+ description: Adversarially critique your own last answer: a blind critic agent re-derives and verifies every claim against live sources, the score is computed from a labelled claim ledger, you revise under a ratchet, restate the corrected answer, and turn each caught error into a durable rule with a fire counter.
4
4
  license: MIT
5
5
  compatibility: Codex, Claude Code, Cursor, and other Agent Skills compatible tools.
6
6
  metadata:
@@ -41,40 +41,102 @@ durable rule has bought every answer after it.
41
41
  Do not use for trivial one-shot replies or simple chat. The overhead only pays off on load-bearing
42
42
  answers.
43
43
 
44
+ **Standalone Cursor installation.** Use the generated Cursor adapter or the full skill package,
45
+ including its `scripts/` directory. Copying only `SKILL.md` is insufficient for this workflow,
46
+ which calls the bundled scripts.
47
+
44
48
  ## Procedure
45
49
 
46
- 1. **Snapshot the target.** Capture your last substantial answer verbatim, plus the concrete claims
47
- it makes and the source each claim rests on. This is `v1`.
50
+ 1. **Snapshot the target.** Capture your last substantial answer verbatim and the original task
51
+ text. This is `v1`.
48
52
 
49
- Write the claim list before you re-read the answer's prose, and put a real receipt against each
53
+ Write your claim list before you re-read the answer's prose, and put a real receipt against each
50
54
  claim: a quoted field, a `file:line` whose body you actually opened, a command and its output.
51
- A claim you cannot receipt at snapshot time is already a finding - mark it `unverified` yourself
55
+ A claim you cannot receipt at snapshot time is already a finding - mark it `unverifiable` yourself
52
56
  rather than waiting for the critic to find it. Grep output, a file name, a path, an endpoint
53
57
  string, or the behaviour of a sibling module are not receipts for what code does; they prove
54
58
  those things exist, not what they do.
55
59
 
56
- 2. **Spawn the critic.** Launch one independent agent with live-source access (issue tracker, code
57
- host, error monitor, the repo). Use the prompt template below. The critic must return: `score`
58
- (0-100), `verdict` (AGREE / AGREE-WITH-CAVEATS / DISAGREE), `where_wrong` (list), `missed`
59
- (related issues or patterns you failed to surface), `unverifiable` (claims it could not confirm),
60
- `fixes` (ranked, actionable), and `shapes` (the reusable class of reasoning behind each
61
- `where_wrong` item, which Step 5 turns into the rule).
62
-
63
- 3. **Decide and loop.** Stop if `score >= 90` OR no actionable `fixes` remain. Otherwise revise the
64
- answer using the fixes to produce `v2`, then re-spawn the critic to score `v2`. Repeat.
60
+ Save the list to `.self-critique/author-claims.json` as an array of
61
+ `{ id, text, load_bearing, receipt }`. Do NOT show it to the critic in round one.
62
+
63
+ **Replay pass.** Before spawning the critic, check the answer against every stored rule. Resolve
64
+ the memory dir as the auto-memory dir for the current project, else the repo's `.claude/memory`;
65
+ if neither exists, skip replay, evidence-check, and prune with a one-line note. Run
66
+ `node <skill>/scripts/rule-store.mjs checklist --dir <memory dir>`, then open each listed rule's
67
+ memory file and read its body before testing the answer against it. The checklist's names and
68
+ descriptions alone are insufficient. Tag each hit `(recurring)` in the report and record it with
69
+ `node <skill>/scripts/rule-store.mjs fire <name> --dir <memory dir> --date <today>`.
70
+ Fire counters and pruning apply only to rules in this memory directory. Rules routed to
71
+ `CLAUDE.md`, `AGENTS.md`, or the shared store are neither counted nor pruned.
72
+
73
+ 2. **Spawn the critic (blind).** Launch one independent agent with live-source access (issue tracker,
74
+ code host, error monitor, the repo). Round one input is the original task text and the answer
75
+ verbatim, nothing else: the critic extracts the claims itself and never sees your list or sources.
76
+ Use the prompt template below. The critic must return one fenced JSON block with `claims` (its own
77
+ list, each `{ text, load_bearing, status, checked_by, receipt }`), `missed`, `where_wrong`,
78
+ `shapes` and `verdict` (AGREE / AGREE-WITH-CAVEATS / DISAGREE). There is no `score` field: the
79
+ score is computed in Step 2b.
80
+
81
+ 2b. **Merge into the ledger.** Match the critic's claims to yours by meaning and set `origin` on
82
+ each: `both`, `author` (only you found it), `critic` (only the critic found it). Every critic
83
+ finding with `checked_by: inferred` must be re-verified by you before it may become `verified`
84
+ or `refuted`; if you cannot verify it, it stays `inferred`. Both statuses require
85
+ `checked_by: command` or `checked_by: read` and a non-empty receipt; validation rejects inferred evidence or a missing
86
+ receipt. Save `.self-critique/ledger-v1.json`, then run
87
+ `node <skill>/scripts/claim-ledger.mjs score .self-critique/ledger-v1.json` and
88
+ `node <skill>/scripts/claim-ledger.mjs summary .self-critique/ledger-v1.json` (an invalid ledger
89
+ exits 2 and prints the errors as JSON).
90
+ Claims found by only one side are reported under their own heading, "Asymmetric claims".
91
+
92
+ Ledger shape (`version` is always `1`):
93
+
94
+ ```json
95
+ { "version": 1,
96
+ "claims": [{ "id": "c1", "text": "...", "load_bearing": true, "origin": "both",
97
+ "status": "verified", "checked_by": "command", "receipt": "quoted output" }],
98
+ "missed": [{ "id": "m1", "text": "...", "load_bearing": false, "checked_by": "read" }] }
99
+ ```
100
+
101
+ - `origin`: `author` | `critic` | `both`
102
+ - `status`: `verified` | `refuted` | `unverifiable` | `inferred`
103
+ - `checked_by`: `command` (run, output quoted) | `read` (file or record opened, quoted) |
104
+ `inferred` (reasoning only). Required on `missed` items too; an `inferred` missed item is not
105
+ penalised. A `verified` or `refuted` claim may not use inferred evidence and requires a
106
+ non-empty receipt.
107
+
108
+ 3. **Decide and loop.** Accept a score of at least 90 only after all fixable findings are resolved
109
+ or explicitly qualified in the answer and report; otherwise continue the critique loop. Stop
110
+ when that condition holds, OR no ledger item is left that a revision could fix, OR 3 rounds have run.
111
+ Otherwise revise the answer to produce `v2`, write `ledger-v2.json`, and run
112
+ `node <skill>/scripts/claim-ledger.mjs ratchet .self-critique/ledger-v1.json .self-critique/ledger-v2.json`
113
+ on the previous and new ledger: a lower re-score discards the revision, and you keep the winner. Round
114
+ two and later hand the critic your receipts for the still-open claims (it is no longer blind).
65
115
  **Hard cap: 3 critic rounds.** (A known ceiling: raise it only if a round is still finding real,
66
116
  fixable defects.)
67
117
 
68
- 4. **Report, always, even after a single round:**
118
+ **Cost.** The critic runs on a mid-tier model (Sonnet class) by default. Escalate one tier only
119
+ when a mid-tier round found nothing and the answer is load-bearing. When the budget is tight,
120
+ verify load-bearing claims first.
121
+
122
+ 4. **Report, always, even after a single round.** In this order:
123
+ - **Corrected answer (`v_final`)** - restated in full, not as a diff, so the user can act on it
124
+ without reading anything else. After a single round with no revision, restate `v1` unchanged.
125
+ - **Claim ledger** - run `node <skill>/scripts/claim-ledger.mjs summary <ledger for v_final>`
126
+ and `node <skill>/scripts/claim-ledger.mjs score <ledger for v_final>` using the ledger for
127
+ whichever version the ratchet accepted (for example, `.self-critique/ledger-v2.json`). Report
128
+ that table and its computed score.
69
129
  - **Where you were wrong** - consolidated, each item with the receipt the critic cited.
70
- - **Iterations to satisfy** - how many critic rounds it took to reach the stop condition (1 means
71
- it was solid on the first pass).
72
- - **Improvement in numbers** - the score trajectory, for example `72 -> 88 -> 94 (+22 pts, +31%)`.
73
- If only one round ran, state `no revision needed`.
74
- - **Final score** - the critic's last score and verdict.
130
+ - **Asymmetric claims** - claims only you found, and claims only the critic found.
131
+ - **Recurring rules fired** - each stored rule the replay pass hit, with its new `fires` count.
132
+ - **Iterations to satisfy** - how many critic rounds it took (1 means it was solid first pass).
133
+ - **Score trajectory** - for example `72 -> 88 -> 94 (+22 pts)`, all computed scores. If only one
134
+ round ran, state `no revision needed`.
75
135
  - **Unverifiable** - anything you should not present as fact.
76
- - **Declined fixes** - any ranked fix you chose not to take, and the reason. Silently dropping a
77
- fix looks identical to missing it.
136
+ - **Declined findings** - ledger items you chose not to fix, with the reason. Silently dropping
137
+ one looks identical to missing it.
138
+ - **Rules written** - where each rule went and what it says (see Step 5).
139
+ - **Rules proposed for deletion** - the candidates from the prune pass, or `none`.
78
140
 
79
141
  5. **Convert the outcome into a durable rule.** See below. Skipping this step is what makes the
80
142
  same critique necessary again next week.
@@ -125,31 +187,50 @@ Before writing to shared persistence, redact the incident evidence, including wh
125
187
  `context-repo` or `shared-knowledge-artifact`, while still including the incident and its dated
126
188
  receipt. This redaction requirement does not apply to local agent memory or repository-rule writes.
127
189
 
128
- **Dedupe before writing.** Read the store first. If a rule of the same shape exists, sharpen it and
129
- add this incident as further evidence rather than creating a second entry. Link related rules
130
- instead of restating them.
190
+ **Dedupe before writing.** Read the store first. If a rule of the same shape exists, add one dated
191
+ evidence line to it instead of creating a second entry, with at most three evidence lines per rule.
192
+ A fourth incident means the rule named the incident, not the shape: rewrite the rule text and keep
193
+ the three most recent evidence lines. Link related rules instead of restating them. Only when the
194
+ memory directory resolved in Step 1 exists, run
195
+ `node <skill>/scripts/rule-store.mjs evidence-check --dir <memory dir>` before writing and report
196
+ any rule it flags. Skip this command when neither supported memory directory exists.
197
+
198
+ **Memory rule identity and counters.** Mark each rule stored by this workflow with
199
+ `rule_store: self-critique` under `metadata:` in its memory file's frontmatter. Mark an existing
200
+ rule only after reading it and confirming it belongs to this workflow. Only marked rules are
201
+ pruning candidates; unrelated project and user memories are excluded. A memory rule's `fires` and
202
+ `last_fired` live under `metadata:` and are written only by `fire`. Rules routed to `CLAUDE.md`,
203
+ `AGENTS.md`, or the shared store are neither counted nor pruned.
204
+
205
+ **Prune pass.** At the end of this step, only when a valid memory directory is available, run
206
+ `node <skill>/scripts/rule-store.mjs prune --dir <memory dir> --idle-days 60`. List the candidates
207
+ in the report under "Rules proposed for deletion". Skip this command when neither supported memory
208
+ directory exists. Never delete a rule without the user's reply.
131
209
 
132
210
  **Then close the loop.** State in your report where the rule was written and what it says, so the
133
211
  user can veto it. A rule the user has not seen is a rule they cannot correct.
134
212
 
135
213
  ## Critic prompt template
136
214
 
137
- Pass this to the critic agent, filling in the two placeholders. It is deliberately adversarial:
215
+ Pass this to the critic agent, filling in the two placeholders (`{{TASK}}`, `{{ANSWER}}`). It is deliberately adversarial:
138
216
  the default stance is that the answer is wrong until a live source proves otherwise.
139
217
 
140
218
  > You are an adversarial reviewer. Your job is to verify and, where possible, **refute** the answer
141
219
  > below. Default to skepticism: assume a claim is wrong until a live source proves it. Address every
142
220
  > comment **directly to the author** ("You claimed X; the source says Y").
143
221
  >
222
+ > **The task the author was given (verbatim):**
223
+ > {{TASK}}
224
+ >
144
225
  > **The answer under review (verbatim):**
145
226
  > {{ANSWER}}
146
227
  >
147
- > **Its claims and the source each rests on:**
148
- > {{CLAIMS_AND_SOURCES}}
228
+ > You are not given the author's claims or sources. Extract the claims yourself.
149
229
  >
150
230
  > **Do this:**
151
- > 1. Re-verify every claim against the **live** source (issue tracker / code host / error monitor /
152
- > the repo) - never trust the answer's own summary. Quote the field, line, PR body, or issue you
231
+ > 1. List every claim the answer makes, marking each `load_bearing` (the user would act on it).
232
+ > Re-verify each against the **live** source (issue tracker / code host / error monitor / the
233
+ > repo) - never trust the answer's own summary. Quote the field, line, PR body, or issue you
153
234
  > checked.
154
235
  > 2. **Check similar and related issues, and find patterns.** Is this part of a cluster? Is there a
155
236
  > closer sibling, parent epic, or duplicate the answer should have surfaced but did not? Name it
@@ -159,19 +240,62 @@ the default stance is that the answer is wrong until a live source proves otherw
159
240
  > a proven root cause?
160
241
  > 4. List exactly **where the author was wrong or imprecise**, and what they **missed**.
161
242
  > 5. Flag any claim you **could not verify** so it is not presented as fact.
243
+ > 6. Label every finding with `checked_by`: `command` (you ran it and quote the output), `read` (you
244
+ > opened the file or record and quote it), `inferred` (reasoning only).
162
245
  >
163
- > **Output, terse, with a receipt for every line:**
164
- > - `verdict`: AGREE | AGREE-WITH-CAVEATS | DISAGREE
165
- > - `score`: 0-100 (rubric: 100 = every claim verified, nothing missed, confidence correctly
166
- > calibrated; subtract for each unverified claim, each missed sibling or pattern, and each
167
- > over- or under-statement)
168
- > - `where_wrong`: bullet list, each with its source receipt
169
- > - `missed`: related issues or patterns the author failed to surface, with IDs
170
- > - `unverifiable`: claims that need a source you do not have
171
- > - `fixes`: ranked, actionable - what to change to raise the score
246
+ > **Output, terse, one fenced JSON block, a receipt for every item:**
247
+ > - `claims`: your own list, each `{ text, load_bearing, status, checked_by, receipt }` with `status`
248
+ > one of `verified` | `refuted` | `unverifiable` | `inferred`
249
+ > - `missed`: related issues or patterns the author failed to surface, each
250
+ > `{ text, load_bearing, checked_by, receipt }`, with IDs
251
+ > - `where_wrong`: bullet list, each with its source receipt and `checked_by`
172
252
  > - `shapes`: for each item in `where_wrong`, the class of reasoning that produced it, stated so it
173
253
  > would apply to a different task ("a conclusion drawn from evidence that does not cover the
174
254
  > claim"), not the incident itself. Say `one-off` when the error has no reusable shape.
255
+ > - `verdict`: AGREE | AGREE-WITH-CAVEATS | DISAGREE
256
+ >
257
+ > Do not output a score. It is computed from your claims.
258
+
259
+ ### Score anchors
260
+
261
+ The score is `100` minus penalties, floored at 0. Calibrate against these three ledgers (all claims
262
+ `origin: both`; `node <skill>/scripts/claim-ledger.mjs anchors` prints them as JSON).
263
+
264
+ **Strong, score 95**
265
+
266
+ | id | load_bearing | status | checked_by |
267
+ | --- | --- | --- | --- |
268
+ | a1 | no | inferred | inferred |
269
+
270
+ **Middling, score 72**
271
+
272
+ | id | load_bearing | status | checked_by |
273
+ | --- | --- | --- | --- |
274
+ | b1 | no | refuted | read |
275
+ | b2 | no | refuted | read |
276
+ | b3 | no | unverifiable | read |
277
+ | b4 | no | unverifiable | read |
278
+
279
+ **Weak, score 40**
280
+
281
+ | id | load_bearing | status | checked_by |
282
+ | --- | --- | --- | --- |
283
+ | c1 | yes | refuted | read |
284
+ | c2 | yes | refuted | read |
285
+ | c3 | yes | unverifiable | read |
286
+
287
+ Penalties:
288
+
289
+ | Finding | Points off |
290
+ | --- | --- |
291
+ | refuted, load-bearing | 25 |
292
+ | refuted | 10 |
293
+ | unverifiable, load-bearing | 10 |
294
+ | unverifiable | 4 |
295
+ | inferred, accepted | 5 |
296
+ | missed, load-bearing | 15 |
297
+ | missed | 5 |
298
+ | verified but found only by the critic | 5 |
175
299
 
176
300
  ## Optional: auto-offer via a Claude Code Stop hook
177
301
 
@@ -217,10 +341,10 @@ message.
217
341
  `iterations: 1, no revision needed` and the single score rather than forcing a revision.
218
342
  - Diversity beats redundancy: if a claim can fail in more than one way, give the critic distinct
219
343
  lenses (correctness, completeness, does-it-reproduce) instead of repeating the same check.
220
- - The critic verifies against live sources; it does not rewrite the answer. You revise; it re-scores.
221
- - Re-score by messaging the same critic rather than spawning a fresh one, and hand it the receipts
222
- for whatever it could not verify in the earlier round, so round two spends its budget on the
223
- claims still standing instead of re-deriving the ones already settled.
344
+ - The critic verifies against live sources; it does not rewrite the answer or score it. You revise;
345
+ the score is recomputed from the ledger.
346
+ - In round two, hand the critic your receipts for the claims still open, so it spends its budget on
347
+ the claims still standing instead of re-deriving the ones already settled.
224
348
  - A critic finding you disagree with is worth one challenge. Put the reasoning to it and let it rule;
225
349
  if it concedes, that is a real answer for the report, and the reasoning is what the user needs to
226
350
  see either way.
@@ -0,0 +1,159 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { pathToFileURL } from 'node:url'
3
+
4
+ export const PENALTIES = {
5
+ refuted_load_bearing: 25,
6
+ refuted: 10,
7
+ unverifiable_load_bearing: 10,
8
+ unverifiable: 4,
9
+ inferred_accepted: 5,
10
+ missed_load_bearing: 15,
11
+ missed: 5,
12
+ only_critic_found: 5,
13
+ }
14
+
15
+ const ORIGINS = ['author', 'critic', 'both']
16
+ const STATUSES = ['verified', 'refuted', 'unverifiable', 'inferred']
17
+ const CHECKED_BY = ['command', 'read', 'inferred']
18
+
19
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
20
+ const isText = (v) => typeof v === 'string' && v.length > 0
21
+
22
+ const enumError = (path, field, value, allowed) => {
23
+ if (value === undefined) return [`${path}.${field} missing`]
24
+ return allowed.includes(value) ? [] : [`${path}.${field} invalid: ${value}`]
25
+ }
26
+
27
+ const claimErrors = (c, i) => {
28
+ const path = `claims[${i}]`
29
+ if (!isRecord(c)) return [`${path} invalid: not an object`]
30
+ return [
31
+ ...(isText(c.id) ? [] : [`${path}.id missing`]),
32
+ ...(isText(c.text) ? [] : [`${path}.text missing`]),
33
+ ...(typeof c.load_bearing === 'boolean' ? [] : [`${path}.load_bearing missing`]),
34
+ ...enumError(path, 'origin', c.origin, ORIGINS),
35
+ ...enumError(path, 'status', c.status, STATUSES),
36
+ ...enumError(path, 'checked_by', c.checked_by, CHECKED_BY),
37
+ ...(['verified', 'refuted'].includes(c.status) && c.checked_by === 'inferred'
38
+ ? [`${path}.checked_by: verified and refuted claims must be checked by command or read`]
39
+ : []),
40
+ ...(['verified', 'refuted'].includes(c.status) && !(typeof c.receipt === 'string' && c.receipt.trim().length > 0)
41
+ ? [`${path}.receipt: verified and refuted claims require a non-empty receipt`]
42
+ : []),
43
+ ]
44
+ }
45
+
46
+ const missedErrors = (m, i) => {
47
+ const path = `missed[${i}]`
48
+ if (!isRecord(m)) return [`${path} invalid: not an object`]
49
+ return [
50
+ ...(isText(m.id) ? [] : [`${path}.id missing`]),
51
+ ...(isText(m.text) ? [] : [`${path}.text missing`]),
52
+ ...(typeof m.load_bearing === 'boolean' ? [] : [`${path}.load_bearing missing`]),
53
+ ...enumError(path, 'checked_by', m.checked_by, CHECKED_BY),
54
+ ]
55
+ }
56
+
57
+ export const validateLedger = (ledger) => {
58
+ if (!isRecord(ledger)) return ['ledger invalid: not an object']
59
+ if (!Array.isArray(ledger.claims)) return ['claims missing']
60
+ if (!Array.isArray(ledger.missed)) return ['missed missing']
61
+ return [...ledger.claims.flatMap(claimErrors), ...ledger.missed.flatMap(missedErrors)]
62
+ }
63
+
64
+ const claimPenalties = (c) => {
65
+ const lb = c.load_bearing
66
+ const byStatus = {
67
+ refuted: [lb ? 'refuted_load_bearing' : 'refuted'],
68
+ unverifiable: [lb ? 'unverifiable_load_bearing' : 'unverifiable'],
69
+ inferred: ['inferred_accepted'],
70
+ verified: c.origin === 'critic' ? ['only_critic_found'] : [],
71
+ }
72
+ return byStatus[c.status]
73
+ }
74
+
75
+ const missedPenalty = (m) => (m.load_bearing ? 'missed_load_bearing' : 'missed')
76
+
77
+ const tally = (keys) =>
78
+ keys.reduce((acc, k) => ({ ...acc, [k]: (acc[k] ?? 0) + PENALTIES[k] }), {})
79
+
80
+ export const scoreLedger = (ledger) => {
81
+ const breakdown = tally([...ledger.claims.flatMap(claimPenalties), ...ledger.missed.filter((m) => m.checked_by !== 'inferred').map(missedPenalty)])
82
+ const total = Object.values(breakdown).reduce((a, b) => a + b, 0)
83
+ return { score: Math.max(0, 100 - total), breakdown }
84
+ }
85
+
86
+ export const ratchet = (prev, next) => {
87
+ const p = scoreLedger(prev).score
88
+ const n = scoreLedger(next).score
89
+ return p > n ? { winner: 'prev', score: p } : { winner: 'next', score: n }
90
+ }
91
+
92
+ const mk = (id, lb, status) => ({
93
+ id,
94
+ text: `claim ${id}`,
95
+ load_bearing: lb,
96
+ origin: 'both',
97
+ status,
98
+ checked_by: status === 'inferred' ? 'inferred' : 'read',
99
+ receipt: 'anchor',
100
+ })
101
+
102
+ // 95 = 5 (one inferred); 72 = 10+10+4+4; 40 = 25+25+10
103
+ const ANCHOR_LEDGERS = [
104
+ { label: 'strong', claims: [mk('a1', false, 'inferred')] },
105
+ { label: 'middling', claims: [mk('b1', false, 'refuted'), mk('b2', false, 'refuted'), mk('b3', false, 'unverifiable'), mk('b4', false, 'unverifiable')] },
106
+ { label: 'weak', claims: [mk('c1', true, 'refuted'), mk('c2', true, 'refuted'), mk('c3', true, 'unverifiable')] },
107
+ ]
108
+
109
+ export const anchors = () =>
110
+ ANCHOR_LEDGERS.map(({ label, claims }) => {
111
+ const ledger = { version: 1, claims, missed: [] }
112
+ return { label, ledger, score: scoreLedger(ledger).score }
113
+ })
114
+
115
+ const cell = (s) => String(s ?? '').replaceAll('|', '\\|').replaceAll('\n', ' ')
116
+
117
+ const claimRow = (c) =>
118
+ `| ${[c.id, c.text, c.origin, c.status, c.checked_by, c.receipt].map(cell).join(' | ')} |`
119
+
120
+ export const summaryMarkdown = (ledger) =>
121
+ [
122
+ '| id | claim | origin | status | checked_by | receipt |',
123
+ '| --- | --- | --- | --- | --- | --- |',
124
+ ...ledger.claims.map(claimRow),
125
+ ].join('\n')
126
+
127
+ const fail = (message) => {
128
+ process.stderr.write(`${JSON.stringify({ error: message })}\n`)
129
+ process.exit(2)
130
+ }
131
+
132
+ const loadLedger = (path) => {
133
+ let parsed
134
+ try {
135
+ parsed = JSON.parse(readFileSync(path, 'utf8'))
136
+ } catch (e) {
137
+ return fail(`cannot read ${path}: ${e.message}`)
138
+ }
139
+ const errors = validateLedger(parsed)
140
+ if (errors.length > 0) return fail(errors.join('; '))
141
+ return parsed
142
+ }
143
+
144
+ const COMMANDS = {
145
+ score: ([a]) => scoreLedger(loadLedger(a)),
146
+ ratchet: ([a, b]) => ratchet(loadLedger(a), loadLedger(b)),
147
+ anchors: () => anchors(),
148
+ summary: ([a]) => summaryMarkdown(loadLedger(a)),
149
+ }
150
+
151
+ const main = (argv) => {
152
+ const [cmd, ...args] = argv
153
+ const run = COMMANDS[cmd]
154
+ if (!run) return fail(`unknown command: ${cmd}`)
155
+ const out = run(args)
156
+ process.stdout.write(typeof out === 'string' ? `${out}\n` : `${JSON.stringify(out, null, 2)}\n`)
157
+ }
158
+
159
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(process.argv.slice(2))
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ // Rule store over a directory of Claude Code memory files (Markdown + frontmatter).
3
+ // Frontmatter is parsed line by line so unrelated lines survive a rewrite byte for byte.
4
+ import fs from 'node:fs';
5
+ import { randomUUID } from 'node:crypto';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const FENCE = '---';
10
+ const EVIDENCE_LINE = /^\s*-\s.*\b20\d\d-\d\d-\d\d\b/;
11
+ const DAY_MS = 86_400_000;
12
+
13
+ const topLevelKey = (line) => /^([A-Za-z_][\w-]*):(?: (.*))?$/.exec(line);
14
+ const metaEntry = (line) => /^ {2}([A-Za-z_][\w-]*):(?: (.*))?$/.exec(line);
15
+
16
+ function splitFrontmatter(text) {
17
+ if (!text.startsWith(`${FENCE}\n`)) return null;
18
+ const rest = text.slice(FENCE.length + 1);
19
+ const closeAt = rest.startsWith(`${FENCE}\n`) || rest === FENCE
20
+ ? 0
21
+ : rest.indexOf(`\n${FENCE}\n`) === -1
22
+ ? (rest.endsWith(`\n${FENCE}`) ? rest.length - FENCE.length - 1 : -1)
23
+ : rest.indexOf(`\n${FENCE}\n`) + 1;
24
+ if (closeAt === -1) return null;
25
+ const front = rest.slice(0, closeAt);
26
+ const body = rest.slice(closeAt + FENCE.length + 1);
27
+ return { frontLines: front === '' ? [] : front.replace(/\n$/, '').split('\n'), body };
28
+ }
29
+
30
+ // top keeps every frontmatter line in order; metadata children are lifted into `metadata`.
31
+ function readTop(frontLines) {
32
+ const top = [];
33
+ const metadata = {};
34
+ let inMeta = false;
35
+ for (const line of frontLines) {
36
+ const meta = inMeta ? metaEntry(line) : null;
37
+ if (meta) {
38
+ metadata[meta[1]] = meta[2] ?? '';
39
+ continue;
40
+ }
41
+ const key = topLevelKey(line);
42
+ inMeta = key?.[1] === 'metadata';
43
+ top.push({ key: key?.[1] ?? null, line });
44
+ }
45
+ return { top, metadata };
46
+ }
47
+
48
+ export function parseRule(text) {
49
+ const parts = splitFrontmatter(text);
50
+ if (!parts) return null;
51
+ const { top, metadata } = readTop(parts.frontLines);
52
+ const valueOf = (key) => {
53
+ const entry = top.find((t) => t.key === key);
54
+ return entry ? (topLevelKey(entry.line)?.[2] ?? '') : '';
55
+ };
56
+ return { name: valueOf('name'), description: valueOf('description'), metadata, body: parts.body, top };
57
+ }
58
+
59
+ function topLines(rule) {
60
+ const hasMeta = rule.top.some((t) => t.key === 'metadata');
61
+ const top = hasMeta || Object.keys(rule.metadata).length === 0
62
+ ? rule.top
63
+ : [...rule.top, { key: 'metadata', line: 'metadata:' }];
64
+ return top.flatMap((t) =>
65
+ t.key === 'metadata'
66
+ ? [t.line, ...Object.entries(rule.metadata).map(([k, v]) => ` ${k}: ${v}`)]
67
+ : [t.line],
68
+ );
69
+ }
70
+
71
+ export function serializeRule(rule) {
72
+ const lines = topLines(rule);
73
+ return `${FENCE}\n${lines.length ? `${lines.join('\n')}\n` : ''}${FENCE}\n${rule.body}`;
74
+ }
75
+
76
+ function readRuleFiles(dir) {
77
+ return fs
78
+ .readdirSync(dir)
79
+ .filter((f) => f.endsWith('.md'))
80
+ .sort()
81
+ .flatMap((file) => {
82
+ const rule = parseRule(fs.readFileSync(path.join(dir, file), 'utf8'));
83
+ return rule && rule.name ? [{ file, rule }] : [];
84
+ });
85
+ }
86
+
87
+ export const listRules = (dir) => readRuleFiles(dir).map((e) => e.rule);
88
+
89
+ export const checklistMarkdown = (dir) =>
90
+ listRules(dir).map((r) => `- [${r.name}]: ${r.description}\n`).join('');
91
+
92
+ export function bumpFire(rule, isoDate) {
93
+ const fires = Number.parseInt(rule.metadata.fires ?? '0', 10) || 0;
94
+ return { ...rule, metadata: { ...rule.metadata, fires: String(fires + 1), last_fired: isoDate } };
95
+ }
96
+
97
+ export function fire(dir, name, isoDate) {
98
+ const entry = readRuleFiles(dir).find((e) => e.rule.name === name);
99
+ if (!entry) throw new Error(`rule not found: ${name}`);
100
+ const updated = bumpFire(entry.rule, isoDate);
101
+ const target = path.join(dir, entry.file);
102
+ const temporary = path.join(dir, `.${entry.file}.${randomUUID()}.tmp`);
103
+ try {
104
+ fs.writeFileSync(temporary, serializeRule(updated), { flag: 'wx', mode: fs.statSync(target).mode });
105
+ fs.renameSync(temporary, target);
106
+ } finally {
107
+ fs.rmSync(temporary, { force: true });
108
+ }
109
+ return updated;
110
+ }
111
+
112
+ const daysBetween = (fromIso, toIso) => (Date.parse(toIso) - Date.parse(fromIso)) / DAY_MS;
113
+
114
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}/;
115
+
116
+ // age signal: frontmatter modified, else created, else file mtime
117
+ function ageDate(rule, mtimeIso) {
118
+ const declared = [rule.metadata.modified, rule.metadata.created].find((v) => ISO_DAY.test(v ?? ''));
119
+ return declared ?? mtimeIso;
120
+ }
121
+
122
+ export function pruneReason(rule, { idleDays, today, mtimeIso }) {
123
+ if (rule.metadata.fires === undefined) {
124
+ return daysBetween(ageDate(rule, mtimeIso), today) > idleDays ? 'never-fired' : null;
125
+ }
126
+ const last = rule.metadata.last_fired;
127
+ return last && daysBetween(last, today) > idleDays ? 'idle' : null;
128
+ }
129
+
130
+ export function prune(dir, { idleDays, today }) {
131
+ return readRuleFiles(dir).flatMap(({ file, rule }) => {
132
+ if (rule.metadata.rule_store !== 'self-critique') return [];
133
+ const mtimeIso = fs.statSync(path.join(dir, file)).mtime.toISOString().slice(0, 10);
134
+ const reason = pruneReason(rule, { idleDays, today, mtimeIso });
135
+ return reason ? [{ name: rule.name, reason }] : [];
136
+ });
137
+ }
138
+
139
+ export const countEvidence = (body) => body.split('\n').filter((l) => EVIDENCE_LINE.test(l)).length;
140
+
141
+ export function evidenceCheck(dir, { max }) {
142
+ return listRules(dir).flatMap((rule) => {
143
+ const count = countEvidence(rule.body);
144
+ return count > max ? [{ name: rule.name, count }] : [];
145
+ });
146
+ }
147
+
148
+ function parseArgs(argv) {
149
+ const [command, ...rest] = argv;
150
+ const flags = {};
151
+ const positional = [];
152
+ for (let i = 0; i < rest.length; i += 1) {
153
+ const arg = rest[i];
154
+ if (arg.startsWith('--')) {
155
+ flags[arg.slice(2)] = rest[i + 1];
156
+ i += 1;
157
+ } else {
158
+ positional.push(arg);
159
+ }
160
+ }
161
+ return { command, flags, positional };
162
+ }
163
+
164
+ const todayIso = () => new Date().toISOString().slice(0, 10);
165
+
166
+ function runCli(argv) {
167
+ const { command, flags, positional } = parseArgs(argv);
168
+ const dir = flags.dir;
169
+ if (!dir) throw new Error('--dir is required');
170
+ const date = flags.date ?? todayIso();
171
+ const commands = {
172
+ list: () => JSON.stringify(listRules(dir).map(({ top, ...rule }) => rule), null, 2),
173
+ checklist: () => checklistMarkdown(dir).trimEnd(),
174
+ fire: () => {
175
+ const { top, ...rule } = fire(dir, positional[0], date);
176
+ return JSON.stringify(rule, null, 2);
177
+ },
178
+ prune: () => JSON.stringify(prune(dir, { idleDays: Number(flags['idle-days'] ?? 60), today: date }), null, 2),
179
+ 'evidence-check': () => JSON.stringify(evidenceCheck(dir, { max: 3 }), null, 2),
180
+ };
181
+ const run = commands[command];
182
+ if (!run) throw new Error(`usage: rule-store.mjs ${Object.keys(commands).join('|')} --dir <dir>`);
183
+ return run();
184
+ }
185
+
186
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
187
+ try {
188
+ console.log(runCli(process.argv.slice(2)));
189
+ } catch (err) {
190
+ console.error(err.message);
191
+ process.exit(1);
192
+ }
193
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-release",
3
3
  "description": "Ship a release: semantic-release setup, changelogs, store listing copy, release-day routine.",
4
- "version": "1.59.2",
4
+ "version": "1.60.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-skill-meta",
3
3
  "description": "Author and maintain agent skills and the AI toolchain itself.",
4
- "version": "1.59.2",
4
+ "version": "1.60.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },