Repository navigation
Releases: hamidpl/lore
Release list
Lore v1.1.3
Three ways documentation could get past the Stop gate, or stay stuck behind it, plus a
Stop hook that cost seconds on every turn. All came from a whole-repo audit on
2026-11-01, and each bug was reproduced in a temporary Lore project before it was fixed.
An open instruction no longer hides behind a backtick
The run-contract check skipped every [u#] row that contained a backtick. So
| u1 | also cover the `/settings` page | pending | | passed, while the same row without
backticks blocked. In the other direction, fenced code blocks were not skipped, so a format
example quoted inside ``` blocked a valid census. Rows are now read with fenced blocks
removed, and inline code is treated as part of the row.
Approved pages are recognised in repos named *-docs
The validator's Files reviewed: line was parsed from the first docs/ in each path. An
absolute path such as /x/acme-docs/docs/a.md became docs/docs/a.md, so an approved page
stayed uncovered. The Stop gate then reported that the page had changed after the last
green run, and every re-run repeated that. The project root is now removed before docs/
is found. The same parser handles the reviser's Files edited: line. It now also keeps
underscores in filenames: before, docs/my_page.md could never be recorded as reviewed.
A docs edit made through the shell is gated like any other
Only the Write and Edit tools marked a session as having produced documentation. A
fresh session could sed -i a validated page and end its turn with the Stop gate never
applying, which is the tree-wide-edit incident the digest gate was built for. These
commands now mark the session too: an in-place sed/perl over docs (including
find docs … -exec sed -i), a redirection into docs/, and tee into docs/. Reading
the docs does not.
The Stop hook is about 9× faster
Each page's digest is now computed once, in one process, and reused by both checks that
need it. The unreferenced-image warning runs one search instead of one per image. With
200 pages and 400 images, a turn end took 2.4 s and now takes 0.27 s (1.2 s → 0.18 s for a
session that wrote nothing). The warnings it prints are unchanged. optimize-images.sh
now uses the hooks' shared digest helper instead of its own copy.
Verified by 11 new tests (456 in total) on macOS and in CI on Ubuntu. Existing projects
need no action.
Lore v1.1.2
A brand-new project could not finish /lore:init without a documentation run's
ceremony. Init rewrites the sample docs/intro.md in the documentation language. That
is an ordinary Write/Edit under docs/, so record-evidence.sh marked the session
as having produced documentation, and the first Stop demanded a source census and a
lore:doc-validator run for a page no source was read for. In the observed run the model
wrote an init-intro-census.md and spawned the validator, which approved a translated
placeholder. /lore:config rewrites the same page from the product description and hit
the same block. Observed in a real project on 2026-10-24.
The starter page is recorded by content, not by session
A new script, scripts/mark-starter.sh, runs after init or config rewrites the page. It
records the page's sha-256 in .claude/sources/.starter-baseline, which is git-ignored.
The Stop gate now treats a session whose only docs/ output is that page, still at that
digest, as having produced no documentation. The validator gate also stops reporting the
page as new and unvalidated. Two properties keep this from becoming a hole:
- The path is fixed (
docs/intro.md). The script takes no page argument, so it cannot
take a real page out of the gate. - The exemption follows the digest, like the validation waiver. The first edit by a
skill, the reviser or a hand makes the page documentation again.
With no digest tool on the machine the page stays gated, as before. The init and config
commands now say explicitly not to write a census or run the validator for this page.
Verified by 11 new tests: the unmarked page is still gated, the marked page passes, an
edit after marking is gated, the starter page next to a real page is gated, a re-mark
keeps one line, and the script refuses a non-Lore tree.
Existing projects need no action. A leftover census for the starter page, such as
init-intro-census.md, records a run that did not happen and can be deleted.
LICENSE is plain MIT
GitHub reported the license as NOASSERTION ("Other") because the Vazirmatn font notice
was appended to the MIT text. LICENSE now holds the MIT text only. The font's OFL
notice stays in the README's License section and in the fonts directory.
Lore v1.1.1
On Claude Code 2.1.30x the validator's green verdict was recorded as UNKNOWN, and
the Stop gate then blocked every turn. The cause was not in the verdict parser. The
runtime now delivers a subagent's result through a SubagentHandback tool call, and
the SubagentStop payload's last_assistant_message holds a stub. The parser read
last_assistant_message, so it was parsing the stub. Re-running the validator could not
clear the block, and a waiver correctly refused a non-green verdict. Observed in a real
project on 2026-10-19.
SubagentStop hooks read the report from the hand-back
A new hook, record-handback.sh, runs on PostToolUse for SubagentHandback, which is
the route the hooks reference documents. It saves a Lore subagent's report under its
agent_id, and it fires before that subagent's SubagentStop (measured on 2.1.301). A new
helper, lore_subagent_report in hooks/lib/common.sh, returns the final message when
it carries the line the caller parses. Otherwise it returns the saved report. If there is
none, it reads the last SubagentHandback call from the subagent's transcript. The
transcript is only a fallback, because it is written asynchronously and may not hold the
call yet. The validator recorder,
the reviser branch and require-worker-evidence.sh all use it. The parsers did not
change. With no JSON tool or no transcript, the hooks behave exactly as before.
The same stub caused two quieter defects, and both are fixed:
- A 5-file validation was recorded as 42/42 reviewed, because the legacy "no
Files reviewed:line means everything was reviewed" fallback fired on a stub. That
fallback now requires a verdict, and a report with no verdict blesses nothing. - A three-file reviser batch was recorded as
REVISED 0 0.
One run, one record
A subagent that ends in plain text is made to call the hand-back, so SubagentStop fires
twice for one run, and the second stop's UNKNOWN overwrote the green receipt. History
lines now carry the runtime's agent_id as a fifth column. A later stop from the same
agent writes nothing in two cases: when it carries no verdict (for the reviser, no edited
files), and when it restates that agent's last result over the same content. A resumed
agent that returns a different result is still a new record. The Stop gate reads only
columns 2–4, so existing history files are read the same way.
The subagent tool is Agent
On 2.1.301 a Task matcher still fires for an Agent call, but the payload's
tool_name says Agent. The scripts compared only Task. As a result
record-evidence.sh recorded no subagent: rows, and guard-under-review.sh never
opened the reviser bracket. Both scripts now accept both names. The hooks.json
matchers name both as well, so the hooks no longer depend on the runtime keeping the
alias. The skills and hook messages now say "Agent tool".
Hook commands quote ${CLAUDE_PLUGIN_ROOT}
When the install path contained a space, an unquoted placeholder split into several
words and the hook did not run. claude plugin validate --strict rejects that form on
2.1.281 and later. All 15 commands now quote the path. The CI wiring check and its test
mirror read the quoted form, and a new test fails on any unquoted command. Verified by
loading the plugin from a directory whose name contains a space: the SessionStart sync
hook ran.
Python in hooks runs isolated
A hook runs from the documentation repo, and python3 -c puts the current directory
first on sys.path. A json.py in that repo would therefore have run, as the user,
whenever a hook fell back to Python for JSON. All four call sites now pass -I. A new
test plants such a module and fails if it is imported. A static sweep fails on any
python3 in hooks/ or scripts/ whose first option is not -I (so python3 -u -c
cannot slip past it), and it runs even on a machine without python3. The rationale
lives in hooks/ARCHITECTURE.md next to json_field.
Maintenance
- Install: the README and the install page (EN and FA) add the one-step form
/plugin install lore --marketplace hamidpl/lore, for Claude Code 2.1.275 or later. The
two-step form stays the default because it works on every version. The README's
versioning paragraph had still called Lore pre-1.0. It now states the policy since
1.0.0, and its pin example points at a current tag. - Site dependencies: Docusaurus moves from 3.10.1 to 3.10.2, and
npm audit fix
brings the docs site from 67 advisories to 45. Most of the rest came in through four
packages Docusaurus pins at vulnerable ranges, where npm's only suggested fix was a
downgrade to 3.7.0. Each is now overridden to its first patched release (tinypool
^2.1.2,serialize-javascript^7.0.5,postcss-selector-parser^7.1.6,uuid
^11.1.1), which takes the docs site to 28 advisories and 0 critical. The 28 left are
allbraces≤ 3.0.3, which has no patched release. Every built HTML page is identical
to the pre-override build apart from asset hashes. None of this reaches the plugin; it
is build-time only. The landing moves to Astro 7.4.0, which
fixes its one critical advisory and leaves one low. Its built page has the same text,
and its CSS differs only by equivalent minifier rewrites. - §7, language-skill automation: the methodology named two false-positive classes in
a language skill's checker, and one of them had since been fixed upstream. The
sentence now states the shape of the risk instead of an inventory: such a tool
normalises text, while a documentation file is text and structure, so expect a prose
rule applied inside a code fence, a table, a URL or a quoted UI string. It reaches
existing projects through the methodology sync on update. - Plugin README: a new section discloses what Lore runs, sends and writes: every
network request (Figma API, Playwright, trusted sources, npm for Docusaurus), the files
hooks and scripts write, and the one permission changesite-to-docoffers. It states
only what the code enforces. - Hook rationale moved from the repository's
CLAUDE.mdinto
plugins/lore/hooks/ARCHITECTURE.md, which ships with the plugin.CLAUDE.mdkeeps an
index. - CI:
actions/checkoutandactions/setup-nodemove from v4 to v7, and
FTP-Deploy-Action from 4.3.5 to 4.4.0, which runs on Node 24. Node 20, which reached
end of life in April 2026, is replaced by Node 24 in every workflow. Astro 7 needs
Node ≥ 22.22.
Verified by running the real lore:doc-validator under 2.1.301 in auto mode with
this plugin loaded. The report came through the hand-back, the hook recorded
APPROVED WITH WARNINGS 1/1, and the evidence log gained an Agent subagent: row. Also
verified by capturing the raw SubagentStop and PreToolUse/PostToolUse payloads under
2.1.301, and by replaying the real incident's two stops through the fixed hook:
APPROVED WITH WARNINGS 5/42 and REVISED 3 3, where the run had recorded
UNKNOWN 42/42 and REVISED 0 0. Thirty new tests cover the hand-back recorder, the stub stop, the
Files reviewed: scope, the double stop, the reviser hand-back, an extraction worker's
hand-back RECEIPT, the no-JSON-tool degrade, and Agent in both recorders.
Lore v1.1.0
Persian documentation written from English sources reads as translated long after
the translation is accurate. Punctuation habits and sentence shapes carry over, and
nothing in the methodology said anything about it — §7 asked only that the content be
in the project's language. This release adds the missing half, and it adds it as a
reference rather than as rules of Lore's own.
§7 defers prose to the language's own writing skill
There is a good open skill for exactly this problem
(persian-writing, MIT). Wiring it in
as a plugin dependency would be wrong twice over: it is 13 MB against Lore's 780 KB,
which every English-only project would pay, and vendoring its rules would fork them and
break Rule 4. So §7 gains a conditional reference instead — when a skill covering
the configured documentation language is available in the session, it is the authority
on that language's prose and orthography, and the methodology restates none of it.
Skills already cite §7 by number, so this reaches every producer with no skill edits.
What Lore keeps is only what a general language skill cannot know: the constraints
Lore's own output format imposes. For a right-to-left language that is two rows, both
learned from a measured run over this repo's own Persian docs:
- Ordered-list markers stay ASCII (
1., never۱.). CommonMark recognises only
0-9as a marker, so a localised one renders as literal text and the list stops
being a list. Persian digits in headings and prose are correct and stay. - The
U+200FRLM before a neutral-opening token has to survive (/lore:init, or
a code span whose content starts with/). The bidi algorithm gives a leading
neutral the paragraph direction and flips the token. An RLM before a strong-LTR
letter is inert and can go.
A language skill's own automation is advisory, not authoritative: run its checker,
read each finding, never run a bulk auto-fixer over the tree. Two classes of false
positive are known to reach correct text — a substring match with no word boundary,
and a whitespace normaliser that ignores fenced code blocks.
Absent any such skill, the floor is the language's own punctuation, digits and
word-joining conventions, and no punctuation pattern carried over from English merely
because the source text used it.
Expected, not blocking. Nothing can judge whether prose reads natively — marking it
⛔ would claim an enforcement that does not exist. The two format rows above are
checkable, and lore:doc-reviewer now runs them: its §7 row reports a localised
ordered-list marker and a missing RLM before a neutral-opening token as warnings,
citing §7 rather than restating it. Whether the prose itself reads natively is
explicitly not the validator's to judge.
Site documentation
review-and-validatecontradictedenforcement-hooks— one said every fix is
made by the main agent, the other that edits go throughlore:doc-reviser. The first
was a 0.9.0 sentence that 1.0.0 superseded, and nothing caught it because neither
page is the canonical home for the rule. The page now matches the methodology, and
keeps the reason the old sentence was worth writing: the actor that produced a claim
is the worst-placed to judge what to change.multilingual-rtlcovered layout only, which is half the story. It now states
the deferral above, namespersian-writing, gives the two constraints Lore keeps,
and repeats the advisory-linter warning.- The Persian documentation was rewritten as native Persian prose, under the rules
above rather than translated after: zero em dashes, zero explicit kasras, linter
clean. Heading parity with the English holds across every page. - Two stale facts corrected in
CLAUDE.md.
Verified: sh tests/run-tests.sh passes; production build passes for both locales.
Lore v1.0.0
0.9.0 named the problem: the loop is the fixing, not the validating. This release
gives the fixing an owner. The rules that came out of that incident — batch the
fixes, don't edit under review, stop after two self-feeding rounds — were prose, and
prose is enforced by nothing. Measured during this release: in both of the first runs
where the routing rule existed only as a sentence, the agent read it and then applied
the batch itself anyway. Everything below follows from making those rules deterministic
instead of hoping.
This is also the first stable release. From here, breaking changes land only in a major.
The fixing has an owner: lore:doc-reviser
A new subagent — the only one in this release, deliberately. It receives the
validator's report as someone else's work order, applies every mechanical and
content finding of a round as one batch, and edits only the files and sections each
finding names, one exact occurrence at a time.
- It cannot fetch, and cannot create files (
tools: Read, Edit, Grep, Glob). That
is the point: the one class of defect a fixer with no network can never introduce is
a fabricated source. A finding that needs new evidence is not a revision — it is
unfinished extraction, and it goes back there. - It re-opens the evidence before applying a fix, and returns the finding
unapplied when the payload contradicts it. A validator's proposed fix is a claim
like any other. - Findings are routed by class. Every
Required Actionnow carries
Class / Targets / Evidence / Counter / Severity / Fix(canonical in the
lore:doc-reviewerskill).mechanicalandcontentgo to the reviser,evidence
back to source collection,decisionto the user. A blocking finding with no
Evidence:entry no longer counts toward the verdict.
Why not let the writer fix its own page: the research behind this release measured
that revision by the authoring context breaks about a third of previously-correct
content per round, and that narrower one-at-a-time feedback breaks more, not
less — 32% at one finding per round against 19% at four. Hence: batch the what,
constrain the where.
Three rules that were prose are now denials
guard-under-review.sh— while the standing verdict is BLOCKED, an edit to
docs/is refused unless it happens inside a reviser run. The run is bracketed by a
hook-written marker on the reviser's ownTaskcall, not by subagent metadata, so
the rule holds on every Claude Code version. A green verdict lifts it immediately and
the existing change gate takes over.check-citation-loss.sh— an edit that would drop a URL whose fetch is in the
evidence log is refused. Citation loss during revision is the one damage class no
prompt-level mitigation cured in the literature, and it is deterministic here only
because the log is hook-written. Judged against the whole file, so a citation moved
between sections survives; a host the log only ever sawmentionedis not a receipt
and is not gated.guard-reviser-edit.sh— denies the reviser aWrite, areplace_all, or any
path outsidedocs/. Where the runtime does not expose which agent is editing, this
one degrades to prose; its inability to fetch is enforced everywhere by its tool grant.
The evidence log has one writer, and says who fetched what
lore_append_evidence in hooks/lib/common.sh is now the single writer of
.claude/sources/.evidence-log; three private copies of that printf used to drift
independently. The line grew two optional columns —
ts tool detail tier [agent] [sha]:
agentis the subagent id the runtime stamps on a hook payload, so a fetch made
by one of several extraction workers is attributable to that worker.shais the digest of the saved payload, andfigma-probe.shnow also records
the HTTP status it had in hand. A receipt proves what came back, not just that
something did.
Every reader uses NF >= 4, so 4-, 5- and 6-field lines are read alike and older logs
keep working.
Figma extraction can fan out — and by default does not
require-worker-evidence.sh refuses to let an extraction worker finish while nothing
backs what it is about to report: no saved payload on disk, no fetch attributed to it.
A worker returns a summary, and a summary with no receipt is exactly what the evidence
model exists to refuse. The merge is deterministic — concatenate the receipt lines, sum
the counts, one row per payload path, disagreements become anomaly rows, and a scope
that came back empty is re-run sequentially rather than filled from memory.
But fan-out is opt-in, because it was measured and it lost. Three parallel workers
against a 36-frame file took 877 s and $3.98 where a single pass took 459 s and
$1.50 — 1.9× slower at 2.7× the cost, with every mechanic working correctly. A
worker's turns dominate at that size, not the API's latency. The regime where it pays
is unmeasured, and the skill says so; lore:site-explorer is never fanned out, because
every site worker would share one browser session.
Two defects the release's own experiment surfaced
- The verdict parser mis-recorded a green round. A scoped re-validation that
recounted the previous round — "Previous verdict: BLOCKED", or a round-summary table
row — was recorded as BLOCKED although it recommended APPROVED, and the loop paid a
whole extra round for it. The last verdict-opening line now wins, and table rows never
set a verdict. - The overclaim tripwire was watching the wrong files. Both READMEs promised more
than the architecture can deliver — that the evidence gates leave a skipped source
nowhere to stay hidden — which contradicts this project's own threat model, where
unforgeability is explicitly not a property it can have. The test that should have
caught it never scanned the root README, the most public file in the repo. Reworded
to what the gates actually do, and the tripwire now covers both READMEs and the
documentation site in either language.
Documentation
The public documentation now describes what the plugin actually does: lore:doc-reviser
and the two new blocking gates are documented on the site in English and Persian,
including why the fixer is not the writer and how findings are routed. Two limits were
added rather than smoothed over — a receipt records the fetch and not the source's later
life, and some reviser rules rest on instruction where the runtime does not expose the
editing agent. Beta wording is retired throughout, and the versioning pages now state
what a patch, a minor and a major may each change.
How this was verified
- 401 automated checks (up from 334), shellcheck clean,
claude plugin validate
passing, and both sites building — documentation inenandfa, landing. - A measured pilot, not a claim. Two arms — the 0.9.0 loop against this one — over
three real documentation projects, three injected defects each, two runs per arm: the
new loop fixed 18/18 planted defects against 16/18, with zero reaching a
green verdict against two, no harmful fixes and no citations lost in either arm,
at +18% cost and +7% wall-clock. That is a pilot at n=6 runs per arm on brief-based
fixtures, and the reviser's headline benefit — less collateral damage — was not
demonstrated, only not contradicted: neither arm produced a harmful fix at this defect
size. It is reported as evidence, not as proof.
Lore v0.9.0
0.9.0
The validation loop was self-feeding, and the tooling could not tell. One real
delivery took seven validation rounds — and four of them found defects that had not
existed the round before. The diagnosis that matters: this is not a validator that keeps
complaining. lore:doc-validator is read-only and changes nothing; the loop lives in the
fixing, which happens under time pressure, one edit at a time, sometimes while a round
is still in flight — and a fix is a claim that passed none of the checks the original
passed.
The receipt now records what was validated, not just that something was
.validator-receipt carried (timestamp, verdict). So a narrow APPROVED — "check these
two strings" — was indistinguishable from a full audit, and the Stop gate, having only an
mtime to compare, had exactly one response to any later change: re-run it. A one-word
typo fix and a 154-hit tree-wide replacement got the same treatment, and neither reached
the user's judgement.
The receipt is now a hook-written per-file snapshot: sha-256 of every docs/ page plus a
coverage status — reviewed (named in the report's new Files reviewed: line),
inherited (unchanged since a previous green receipt; never inherited from a BLOCKED
run), waived, or uncovered. The digest snapshot alone never grants coverage: a
file the validator never opened stays uncovered no matter how many green runs pass over
it. Line 1 keeps the old ts<TAB>verdict shape, so an old reader still works; with no
sha tool on PATH the hook writes the v1 receipt and the gate falls back to mtime.
The Stop gate compares content, and asks instead of looping
find docs -newer <receipt> was wrong in both directions. A git pull, a rebase or an
editor save that restores identical bytes forced a whole re-validation round for nothing;
and a real change only ever produced "re-run it". Now:
- an unchanged rewrite is a non-event — the false blocks are gone;
- a real change (edited / new / deleted / uncovered) blocks with a message that names the
files and tells the model to report the change and its risk to the user and ask —
offering a scoped re-validation of exactly those files, or a user-approved waiver.
The waiver (.claude/sources/.validation-waiver) names each file at its exact current
digest, so a later edit silently invalidates it: an approval can never stretch to cover
a change the user never saw. It cannot launder a non-green verdict, and the next validator
run folds it into the receipt and deletes it.
The delivery boundary, batched fixes, and a circuit breaker
New in the Auto-Validation Rule (canonical in lore-methodology.md): a green verdict
ends that delivery — a later edit is the next one; fixes go in as one batch
followed by one scoped round, and no file is edited while it is under review (the two
rounds that finally came back clean in the real run were the two that did this); and
two consecutive rounds finding fix-introduced defects means stop and put the decision to
the user, not open a third. The producer skills' old instruction — "fix and re-run
until it returns green" — was itself the loop, and is gone. .validator-history is
append-only, so "how many rounds did this take" is now a number rather than a memory.
Zeros: prove the probe, don't name a flag
Two independent mechanisms made the validator's own searches silently return nothing: the
environment's grep skipped the evidence corpus because raw/ is git-ignored, and some
payloads store text as \uXXXX escapes where a raw UTF-8 needle cannot match. Both exit
cleanly with zero hits, and no portable flag fixes both. §0.2 therefore prescribes a
control needle — a string already proven present, through the same command over the
same paths — rather than any tool or flag. Note the shape: the tool enforcing §0.2 was
itself failing §0.2.
That rule had to land first, because of what it gates: the validator now runs an
always-on quoted-string provenance sweep (a fabricated or mis-attributed UI string was
this project's most repeated defect, and was only ever caught when someone asked for it by
hand). A BLOCKING sweep over a broken probe is worse than no sweep — in one round it
nearly condemned a dozen correct strings as fabricated — so "not found" is a verdict only
after the control passes and a tolerant retry (zero-width marks, escape forms, Unicode
variants) still finds nothing.
Also
- Nothing under
docs/is evidence about the product (new, under §0.1). Citing our
own normalised output to prove what the product does is how a correct sentence got
"fixed" into a wrong one across 21 pages. And a fix is a new claim, carrying the
same receipt obligation as the claim it replaces. remind-mass-edit.sh(new, advisory): before a tree-wide identical edit ofdocs/,
injects the checklist the incident produced — fidelity-promise wording, text whose
subject is the replaced word, rows that become duplicates, Unicode variants the
replacement misses. No output check can see this damage: nothing is mis-typed, every
changed line is correct, only the surrounding sentence became false.- Standing product decisions get a home. A permanent ruling frozen into a
[u#]row
diverged silently months later, because those rows are checked once and never again.
They now belong in the product layer with their date;check-census.shblocks a
Standing:row that references nothing. - Findings now carry a provenance label (
pre-existing/introduced-since-last-green),
which is what separates "the validator keeps complaining" from "our fixes keep breaking
things" — and feeds the circuit breaker.
The edge-case sweep now produces a list a designer can act on
brief-to-doc already walked the edge-case taxonomy — but it wrote for the wrong reader.
Its output was questions aimed at the document's author, scattered through the scenarios
as [CLARIFICATION NEEDED] markers. A designer opening the document found no list of
what still had to be drawn.
Every applicable category now produces two things: the marker at the step it belongs
to, and a row in the template's new ## States to Design table — the flat checklist,
with three statuses that separate a design task from a product decision:
specified — needs design, unspecified — needs decision + design, designed.
figma-to-doc's missing-states check feeds the same table instead of only inline markers.
Two things that had to be said explicitly for this to work at all:
- Naming a required state is not inventing behavior. The no-invention rule forbids
writing what the product does in an undescribed state; the table writes none of that —
it names which state needs designing. Without the carve-out stated in the template,
the two rules appear to conflict and the table gets left empty. - The table is exempt from the ~5-question cap. Asking costs the user's attention;
listing costs nothing. Trimming the designer's list to match the number of questions
asked was the obvious wrong move.
Also fixed: the canonical taxonomy had no loading category, while figma-to-doc
carried its own four-item copy that demanded a loading state. Two lists, silently
diverged, nothing able to catch it. There is now one list, plus a test asserting no skill
re-lists it and that every category a skill names exists in the template.
Images are compressed before they are committed
There was no image optimization anywhere. Figma exports at scale=2 (a 1440px frame
lands as 2880px) and Playwright screenshots are raw PNG — and both get committed to the
documentation repo and served by the site, so nothing reclaims that weight later.
scripts/optimize-images.sh runs once per run, after every capture has landed.
Measured on real UI screenshots: 72% smaller, dimensions unchanged, no visible
difference.
- One batch, not per image — and not for the obvious reason. Process startup is
~10ms; what per-image really costs is one agent tool round-trip each, so forty
images means forty round-trips instead of one call. - The quality floor is the safety property.
pngquant --quality=65-90exits 99 and
leaves the file untouched when it cannot hold the floor, so "don't damage quality" is
enforced by the tool, not by anyone's judgement. A lossless pass follows. PNG stays
PNG — WebP would save more but changes every extension, doc reference and census row,
so it is deferred to its own change. - Idempotence is correctness, not speed. A second lossy pass degrades the file
again, so the fingerprint of each optimized result is recorded and skipped next time.
A re-captured screenshot has a new fingerprint and is optimized afresh.
With no optimizer installed it reports tool=none, changes nothing, and exits 0 — never
claiming a saving it did not make, and never failing a run over a missing optional
binary. The Stop hook warns (never blocks) about large PNGs that never went through it.
Lore v0.8.1
The first release driven by measurement rather than reasoning. A workflow audit
proposed twenty optimizations. Experiments rejected seventeen of them — two of which
would have opened holes — and a benchmark showed the headline saving of an eighteenth
never materialised. What shipped is the residue that survived: three small changes, each
traceable to a number.
The validator re-read rules it already had
doc-validator opened .claude/CLAUDE.md and .claude/lore-methodology.md on every run.
Both are already in its context: CLAUDE.md @-imports the methodology, imports are
expanded at launch, and a non-fork subagent inherits the whole CLAUDE.md hierarchy. The
prompt even conceded the point — "both are in context — but read both files explicitly".
Verified before changing: a session scaffolded from the docs layer quoted §0.2's first
bullet with no tool use, and a subagent spawned inside it did the same, reporting it had
the text "without reading any file". Instrumented runs confirm the behaviour changed —
baseline read both files, current reads neither.
were genuinely avoided, and the freed budget went to other files instead. Recorded here
because the opposite is the intuitive assumption and it is wrong.
One malformed command cost an entire batch
Tracing a full run found 12 of 40 tool calls failing. The mechanism: test -s X && echo Y
is rejected as "multiple operations" and wc -w < file as "input redirection" — and
when one call in a parallel batch is rejected, every sibling issued alongside it is
discarded. Two malformed commands destroyed eleven healthy ones and produced three
round-trips that gathered nothing.
doc-validator now requires one operation per Bash call, no chaining, no <. It also
records a rejected command as unrun instead of reissuing it — the same curl had been
retried three times.
Measured, same fixture: 154.1 s vs 216.8 s wall-clock, 32 vs 40 tool calls, 6 vs 12
failed calls, 41,467 vs 43,990 tokens. Zero malformed commands in the post-change run.
Single runs, and duration varied 190–248 s across identical configurations, so treat the
timing as indicative and the counts as firm. Reasoning turns did not change: 10 → 10.
Two maps of the same territory
lore-methodology.md carried two canonical-locations tables — one under Rule 4, one under
the DoD declaring "Nothing else may restate it" — and two copies of the input-type→skill
table. They are now one each, merged as supersets so no row was lost. This is a
consistency fix, not a saving: it recovers 274 bytes, against the ~400–600 tokens the
audit predicted.
What was deliberately not done
Stop-gate scoping was the audit's #3 item; .docs-touched has exactly one writer
(Write|Edit), and its own comment records that it exists so a git pull does not
gate — inverting it would have let changes arrive unchecked. Evidence-log rotation
contradicts the documented append-only invariant. Dropping the validator's hook-covered
checks assumed hooks had run on those artifacts; check-census.sh fires only on write, so
a carried-over census is unvalidated. All three were rejected on evidence, not taste.
Known, unfixed
doc-validator.md calls lore:doc-reviewer "the single source of truth for how to
review" and gives no path, no env var, and no Skill tool to invoke it. Stale plugin
versions persist in the cache — 0.6.3's copy still says the canonical rules live in
CLAUDE.md, which stopped being true in 0.6.0. Latent while the copies match; live on any
machine mid-upgrade. It is a defect, not an optimization, and needs its own change.
Lore v0.8.0
Post-release correctness and the tests that would have caught it. 0.7.0 shipped the
evidence gates; this is the follow-up the roadmap deferred out of it — the generated-site
defects that predate the release, the hook-layer duplication that let one bug exist in
seven places, and the first tests in this project that check the model's prescribed
workflow rather than a hand-written fixture.
The generated site was never deployable as generated
-
urlwas the framework's own example domain. Every site Lore scaffolded shipped
url: 'https://your-docusaurus-site.example.com'— the origin of every canonical
<link>, everyog:tag and every entry insitemap.xml. Wrong, and wrong in a way
that looks deliberate, so nothing ever flagged it. It is now a{{SITE_URL}}
placeholder the wizard must fill: it asks where the docs will be deployed, and
writeshttp://localhost:3000when the answer is "not yet" — obviously provisional
beats plausibly wrong. -
onBrokenLinkswas'warn'. DoD §6 names the production build as its
authoritative link check and the install guide repeated that promise, while the build
went green with dead links in it. Now'throw'.⚠️ Behaviour change for consumer projects. A site with pre-existing broken links
will now fail its build. That is the point, but it will surface on the first build
after a scaffold refresh rather than at a time of your choosing.
One bug, seven copies
hooks/lib/common.sh. The JSON-extraction ladder, project-root resolution, the
Lore-marker guard and the carve-out were copy-pasted into seven of the nine hooks —
about a third of the hook layer. They now live in one sourced library. Hooks resolve it
through$(dirname "$0"), never${CLAUDE_PLUGIN_ROOT}:hooks.jsoninterpolates
that into the command string, which does not guarantee it reaches the hook's
environment. A test asserts no hook gets this wrong.- The text fallback returned the wrong file. With neither
jqnorpython3
available,json_fieldfell back tosed -n 's/.*"file_path".*/\1/p..*is greedy,
so it returned the last match in the payload — thetool_responseecho rather than
tool_input— on everyWrite, and thehead -n 1after it was decoration. It now
anchors to the parent key and takes the first match. - …and it could not read a boolean.
stop_hook_activeis a JSON boolean, and the
string-only scan returned empty for it. On a machine with no JSON parser, the Stop
hook's loop guard was silently disabled — the one failure mode that can trap a user in
a loop. The fallback now reads unquoted scalars. json_fieldtakes one argument (a key path) instead of three dialect-specific
ones, so a call site can no longer describe the same field three inconsistent ways.
Statements that contradicted their own implementation
- Rule 5 claimed a blocking tier it never had. The methodology listed
Claude/Anthropic/Playwright alongsidelore:*and.claude/paths as forbidden, while
both the hook and the validator deliberately treat the former as warnings — a product
may legitimately document an integration. The rule now states the two tiers it actually
has. doc-validatorforbade and required the same command. One bullet granted Bash "for
npm run build" and forbade "any command that modifies the filesystem". The build
writesbuild/and.docusaurus/. Split into a grant and a constraint, with the build
named as the one explicit carve-out. Its Rule-4 line also still pointed rule citations
atCLAUDE.md, which stopped being where the methodology lives in 0.6.0.remind-census.shstripped every§. The just-in-time reminder — whose entire job
is to point at §0.1–§0.4 — readDoD 0,CLAUDE.md 1,(0.4).brief-to-doccited "§2 step 6" for the readiness gate, which is step 7.detect-project.shcalled the model-written.claude/lore.json"authoritative".
scaffold.sh failed silently
--target X with no --layer printed done (layers:) and exited 0 — a wizard step
that dropped its layer flag reported success over an empty directory. --target with no
value died on shift 2 under set -e with no message at all. Both are now exit 2 with
an actionable message, and an unknown layer is rejected during argument parsing rather
than mid-copy (where it triggered a "already-copied files were kept" warning describing
a copy that never began).
Tests: 173 → 233 assertions
The strategic item, and the reason this release is worth its size. Every defect in the
0.7.0 audit coexisted with a green suite, because each assertion tested a hook against a
fixture written by whoever wrote the hook — so they agreed by construction.
- The skills' own census skeletons are now parsed out of the
SKILL.mdfiles and
checked against what the hooks actually read — that thestatuscell is in the
columnverify-docs.shreads by position, that every Figma manifest source type has a
row, that every zero-case phrase a skill teaches is one the hook recognises. This is
the class of defect that shipped in 0.7.0: a skill prescribing a census its own gate
rejects. - A complete, receipted census per input type — real raw payloads on disk, real
verifiedevidence-log entries — proven to pass both the write-time and Stop gates.
Every prior test proved a gate blocks; none proved they can all be satisfied at once. - A defect corpus mutated from that same green census, one defect at a time, so a
block can only be attributed to the defect introduced. Each mutation is asserted to
have actually landed first — asedthat matches nothing exits 0 and copies the file
through, after which "it blocks" would have been a lie. - CI now lints with
shellcheck -xso the shared library is checked in the context of
each hook that sources it, and asserts the library ships and is not executable.
Lore v0.7.0
Evidence gates: a claim now needs a receipt. Three BLOCKING rules were skipped
in a single real documentation run, and validation passed all three. This release
moves the §0 obligations out of prose and into the deterministic layer.
Problem
Every §0 obligation was discharged by writing a sentence, and no sentence was ever
compared against a machine-observable fact. nothing relevant — confirmed searched is
byte-identical whether a source was read or never touched, and the agent that skipped
the work is the one authoring the sentence — so it always passed. Concretely:
- Figma annotations were never extracted, and the miss was certified. The plugin
hard-codedannotations[].notesin five places; the Figma REST API returns the text
inlabel/labelMarkdown, and its own OpenAPI spec declaresAnnotationsTraitas
properties: {}— the shape is undocumented. The probe returned nothing, and §0's
explicit-zero rule then dutifully recorded0 annotations — confirmed none present.
The census laundered a code bug into a certified absence, deleting every business
rule the designers had written. A second bug compounded it:depth=1was recommended
for scoping, but annotations hang off deep descendant nodes, so that fetch structurally
cannot see them. - A trusted source was skipped and the skip was recorded as a finding. It was assumed
to need a support session because of where its link sat in the page footer, and the
census row readnothing relevant — confirmed searchedwithout a single HTTP request. - An explicit user instruction evaporated.
site-to-docwas asked to cover the
signed-in state; only the guest view was ever observed, and the documentation was
written from it. Nothing on disk remembered the instruction, and the word "guest" did
not appear anywhere in the plugin — auth was modelled as an obstacle to get past, not
as a documented product state. The persistent browser profile actively hides the guest
view on re-runs, so neither state was reliably captured.
0.6.2 addressed this class with "add a rule + have the validator check it". That is the
approach that leaked here, so this release does not repeat it.
What changed — four evidence rules
DoD §0 gains four sub-rules (additive; no renumbering), propagated to existing projects
by the SessionStart sync hook:
- §0.1 Evidence, not attestation — every census row carries a receipt: the probe run,
its HTTP status, the bytes returned, and the on-disk path of the saved raw payload. The
words confirmed / verified / checked carry no evidentiary weight anywhere. - §0.2 Negative-result protocol — a zero is the cheapest result to produce and the one
that silently subtracts content. It is valid only beside a successful receipt and a
corroboration by a second, independent method. Raw payload has the data but the parse
returned 0 → parser failure, not absence. - §0.3 No assumption about accessibility — a source may be called login-gated or
unreachable only on an observed status code or auth wall; never inferred from a
link's position, a name, or a guess. - §0.4 Run contract — explicit user instructions become numbered
[u#]rows written
at pre-flight, and delivery is blocked while any row is open.
Enforcement — the hook layer, not more prose
Hooks can match WebFetch, Task, Bash, MCP tools, and SubagentStop by agent type,
which makes the decisive artifact possible: an evidence log the model does not author.
It is a guard against a step being skipped, not a tamper-proof record — an agent with shell
access can append to any file. The property it buys is that a source never fetched does not
end up in the log by accident, which is the failure mode this release is about.
record-evidence.sh(PostToolUse, never blocks) appends one tiered line per fetch and
subagent run to.claude/sources/.evidence-log— see "The evidence log is tiered" below.check-census.showns every census rule, in two modes: as a Write|Edit hook (exit 2) it
validates shape — Run contract present, a receipt on every trusted-source row,
raw payloads that exist and are non-empty, every claimed URL backed by a verified fetch
in the evidence log, cited screenshots on disk, and no zero the raw payload contradicts;
as--complete <file>it is invoked by the Stop hook to validate completeness — a site
census showing at least one observed state, a Figma census carrying a counted row per
manifest source type.record-validator-run.sh(SubagentStop, never blocks) records eachlore:doc-validator
run and its verdict, matched at the start of a line so report prose cannot set it.remind-census.sh(PreToolUse, never blocks) injects the §0 obligations at the moment a
newdocs/page is created without a census.verify-docs.sh(Stop) gains four BLOCKING gates: no census at all, an incomplete
census, an open[u#]row, and documentation that changed without a fresh green validator
run. All fire only where.claude/sources/exists and this session produced
documentation, so hand-maintained docs trees — and anyone who merely cloned the repo —
are untouched.
Enforcement — the Figma probe
New scripts/figma-probe.sh replaces improvised per-run curl and JSON walking. It saves
the raw payload before anything interprets it, prints the §0.1 receipt, extracts
annotations schema-agnostically (selecting on the presence of a non-empty annotations
array and dumping whole objects — no annotation field name appears in the script at all),
never sends depth, and exits 4 when the raw bytes contain annotation data the parse
missed. A parse subcommand re-analyses a saved payload offline.
Enforcement — adversarial review
lore:doc-validator and lore:doc-reviewer change stance from "check the census is
well-formed" to "assume the census is fabricated; disprove it": re-probe every §1
source and compare the observed status to the claim (a row claiming "requires login" that
answers 200 is a fabrication — blocking), test every raw payload, cross-check the evidence
log, challenge every zero against its payload, verify every [u#] row's evidence exists,
and — for site runs — confirm every scenario with a signed-in Precondition traces to a
signed-in observation row. The report now states which re-verifications actually ran,
because an unrun check is not a passed check.
Auth state is a documented state
site-to-doc gains an Observation coverage matrix (auth state × role × route × viewport,
each row backed by a screenshot), a rule that both the guest and signed-in views of an
auth-gated route must be covered or explicitly waived by the user, and a warning that the
persistent browser profile hides the guest view. lore:site-explorer must now verify and
report which auth state it is actually in before running a scenario — an expired session
serves login walls that look like ordinary pages.
Rule 4: an explicit carve-out for enforcement
Restating a rule near the point of action is exactly what makes it land — and exactly what
Rule 4 forbids. That tension is now resolved explicitly rather than left to judgement, in the
same shape as the existing Rule 3 carve-out: a rule's statement and rationale stay
canonical in lore-methodology.md; an operational instantiation (the columns to fill, a
checklist item, a hook's runtime error message) belongs where the work happens. The test is
"if the canonical rule changed, would this other copy become wrong?" — if yes it is a
restatement and must become a §N reference. The skills, doc-validator, doc-reviewer,
skill-template.md and this repo's CLAUDE.md were rewritten against that test.
Cost and false-block control
Both were measured rather than assumed:
record-evidence.shsits on theBashmatcher — the highest-frequency tool there is — so it
checks its Lore-project guard and a cheap payload filter before starting any JSON parser.
A call in an unrelated repo, or an ordinarygit status, costs ~8.5 ms (down from ~16 ms,
and now essentially just theshspawn). Only a call that actually carries a URL or a
subagent type pays the full ~26 ms — and those already involve network I/O.- The Stop gate is scoped to sessions that actually wrote documentation. Without this, a
git pull, a rebase, or an editor save that toucheddocs/would leave the tree newer than
the validator receipt and block the next turn of an unrelated session.record-evidence.sh
records the session id on adocs/write and the Stop hook consults it; a session that wrote
no documentation is never gated. It still fails closed when no marker exists yet.
The census is now required, and validated twice
The gap that made everything above optional: nothing checked that a census existed.
Every §0 rule is enforced against that file, and check-census.sh only fires when one is
written — so a run that wrote documentation, ran the validator, and simply never wrote a
census delivered cleanly, having proved nothing. The entry point to the deterministic layer
was itself prose. A session that produced documentation must now leave a census, or the turn
is blocked.
Validation is progressive, because a census is written incrementally:
- At write time — shape. The skills order the Run contract written at pre-flight, before
any fetching. Completeness used to be enforced here, so that prescribed write was blocked;
the cheapest escape was a placeholder (an empty## Observation coverageheading, or one
prose line naming the six Figma source types), and the placeholder then satisfied the check
for the rest of the run. The gate was teaching the model to disarm the gate. - At Stop — completeness. The run is over, so a site census must show at least one
observed state and a Figma census m...
Lore v0.6.3
Submission readiness for the Claude Code community marketplace. Anthropic's
review pipeline reads the plugin directory on its own terms — it runs
claude plugin validate and expects the plugin to document itself — so the two
places where Lore relied on the surrounding repository are closed.
The plugin directory now documents itself
- Problem.
plugins/lore/had noREADME.md. Every install, usage, and
contribution instruction lived in the repository root README, one level up.
A reader — or reviewer — landing on the plugin directory found a manifest and
four source folders with nothing explaining what they were. - What changed. Added
plugins/lore/README.md: the component table
(commands, skills, subagents, hooks) and pointers to the root README, the
website, andCLAUDE.md. Per Rule 4 it references the install/update
commands rather than restating them — the root README stays their single
source of truth.
homepage points at the product site
- Problem. The manifest's
homepageandrepositorywere both the GitHub
URL, so the plugin manager surfaced the source tree where it could surface
the product page. - What changed.
homepageis nowhttps://lorekit.net;repositorystill
points at GitHub. A version bump ships with it because consumers only receive
manifest changes whenversionmoves.
Verified. claude plugin validate ./plugins/lore passes, every relative
link in the new README resolves to a file that exists, and sh tests/run-tests.sh
plus CI pass unchanged.