Skip to content

Releases: AndrewBogdanovTSS/evidence-layer

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 20:45

evidence-layer 0.14.0

A command's kind is read from what runs, not from the arguments it was given.
Nothing here changes what an exit code means.

Artifacts

  • classifyCommand ignores paths, quoted strings and leading NAME=value
    environment assignments.
    It matched against the whole command line, so a
    lint run over src/typecheck.ts was classified types, and a test run over
    src/lint/ was classified lint. Besides the misleading id prefix and
    suggested claim ("Typecheck reports no errors" for a lint run), a test or lint
    run classified as anything else was exempt from the work-summary rules: a
    success claim on it was never held to its counts. Now
    pnpm test src/typecheck.test.ts is tests, and its success claims are.
  • A file extension at the end of a token (tsconfig.json, a...b) or a path
    separator anywhere in it marks the token as an argument. Program names,
    subcommands and flags are still read: node node_modules/nuxt/bin/nuxt.mjs typecheck is types, pnpm install --frozen-lockfile is lockfile.
  • An unquoted, path-free argument is still read as part of the invocation:
    pnpm test -t typecheck is types. Quote it.

Behaviour changes to check when upgrading

  • Some artifact ids change. The kind is the id's prefix, so a command whose
    kind came from an argument - for example pnpm eslint src/typecheck.ts, once
    types-…, now lint-… - gets a different id. An artifact generated with an
    earlier version and verified with this one fails its address check for those
    commands only; regenerate the evidence. Commands whose kind did not change
    keep their ids.
  • Success claims on those runs are now held to their work summary, as any
    other lint or test run's are.

Repository

  • The suite is 503 tests across 38 files.

Full Changelog: v0.13.0...v0.14.0

v0.13.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 07:48

evidence-layer 0.13.0

Misses can be recorded from the history of a given ref instead of HEAD.
Nothing here changes what an exit code means.

Misses

  • recordNewMisses(repo, journal, { ref }) and findMisses(repo, { ref })
    read the history of ref - typically the branch a pull request targets. A
    branch merged by squash loses its own commits: a miss recorded from a pull
    request branch's own commit would name a SHA the main branch never contains,
    and the squash commit carrying the same trailer would later be recorded as a
    second miss. Recording from the target's history counts each miss once, by
    the commit that landed.
  • A ref git cannot resolve throws instead of returning no misses: "none" and
    "could not read the history" are different answers. Without a ref, a
    directory that is not a repository still yields none, as before.
  • History is read through git() with an argument array and
    --end-of-options, so a ref is never seen by a shell or read as a flag.

API

  • New: MissesOptions. findMisses and recordNewMisses take an optional
    MissesOptions.

Repository

  • The suite is 489 tests across 38 files.

Full Changelog: v0.12.0...v0.13.0

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 07:36

evidence-layer 0.12.0

Recording a miss is now deterministic. Every field of a journal line made from
a Missed-By: trailer comes from the fixing commit, so any checkout of the
same history writes the same lines in the same order. A project whose main
branch only takes pull requests can record misses from each pull request
branch instead of from one machine. Nothing here changes what an exit code
means.

Misses

  • A miss's id is its commit. recordNewMisses writes the fixing commit's
    first eight hex digits as the entry id (missEntryId), and its committer
    date as ts. Random ids made the same miss a different line on every
    machine that recorded it, so two branches that both recorded it merged two
    entries and the floor counted it twice. Identical lines from two branches
    merge as one.
  • Oldest first. New misses are appended in commit order, not git log's
    newest-first order, so the journal reads in time order.
  • Misses recorded before this release keep their random ids; the dedupe still
    keys on the commit, so nothing is recorded twice.

API

  • New: missEntryId. ExtractedMiss has an optional date;
    parseMissesFromLog reads %H<FS>%cI<FS>%B<RS> and still reads the older
    two-field form.
  • appendEntry accepts an optional id; without one it generates a random id
    as before.

Repository

  • The suite is 487 tests across 38 files.

Full Changelog: v0.11.0...v0.12.0

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 18:27

evidence-layer 0.11.0

A review posted as a pull request comment no longer has to open on command
logs. The claim and sample skeletons can put each artifact block, and the
Sample integrity table, in a <details> fold, with the Claim and Grounding
lines left outside it. The option is off by default, so existing output is
unchanged, and nothing here changes what an exit code means.

Folded skeletons

generateClaimSkeleton(artifacts, { fold: true })
generateSampleSkeleton(samples, omittedCount, { fold: true })
  • Each artifact block sits under its Grounding line in a fold summarised as
    📄 Output - <command> - exit N, L lines, where L counts the block's lines
    between its fences and a stopped run reads exit 124 (timeout).
  • The Sample integrity table's fold summary takes the label's place and says
    how many files it quotes, never which. parseReceipt finds the table by its
    header row, so a folded receipt verifies as before.
  • A summary never names a path. It sits outside every fence, so a verifier
    reading prose for citations would take a path there for a file the review
    cites. summaryCommand keeps environment assignments and drops
    NODE_OPTIONS, --flags and path-like words; the full command is still the
    block's first line.
  • Every fold has an 8px spacer under its summary and blank lines around its
    content: CommonMark ends an HTML block only at a blank line, so without them
    the fence or table inside would not be read as Markdown. The native
    disclosure triangle is kept - it is the control's only affordance for
    keyboard and screen reader users.

API

  • New: SkeletonOptions, foldDetails, summaryCommand.
    generateClaimSkeleton and generateSampleSkeleton take an optional
    SkeletonOptions.

Repository

  • The suite is 483 tests across 38 files.

Full Changelog: v0.10.0...v0.11.0

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 11:22

evidence-layer 0.10.0

Fixes from an independent review of the runtime gate, the gatherer and the
seal - the path that runs inside an agent's session. The gate now says each
thing once, names the command and base it wants gathered with, and costs one
tree fingerprint per stop instead of one per checked text. The gatherer's idea
of "the change" now includes what is not committed yet, and each step can have
its own timeout. Nothing here changes what an exit code means.

Four changes in the same review alter what the gate accepts rather than fix a
bug - what a stop checks, which runs a claim may cite, a restamp command,
and what a fence's commit label means. They are drafted as a proposal in
docs/decisions/0010-gate-scope-and-run-identity.md, not implemented.

The runtime gate

  • Warnings get their own lines. An allow whose only findings were warnings
    (claims/shotgun, a stale git diff block) printed "not everything could be
    checked:" and then nothing. Every non-pass finding is now listed, worst
    first, and an allow without unverifiable findings says "these need a look".

  • An exhausted episode speaks once. After a retry episode spent its blocks,
    every later stop repeated the same systemMessage and journalled another
    retry-exhausted allow. While the checked texts, the sealed run, the tree
    and the findings are what they were at the last verdict, a stop now says
    nothing and journals nothing. Any change - an edit to the review, a new
    gather, an edit to the code a cited run describes, a fetch that changes a
    receipt's base check - and it speaks again.

  • A stop with nothing left to check closes the episode. When the claims
    that opened an episode are gone (the review deleted or put back, the final
    message making none), the episode closes and is journalled with the new
    outcome closed-unchecked - an unverifiable entry, never a clean check.
    A later failure starts a fresh episode, with fresh blocks.

  • Untagged blocks are listed, for the person. journal report --include-gate lists every block not tagged yet, newest first - id, time,
    scope, first finding - so journal tag <id> real|false --gate needs no
    reading of the JSONL. It also counts closed-unchecked episodes on their own
    line: an upper bound on blocks escaped by dropping the claims, since a change
    of subject after a block counts too. The id is never put in a block's
    reason: the agent reads the reason, and the blocked party must not be handed
    the command that grades its own block.

  • The gather command is named, with its base. Session start, a block and
    seal/no-run say which command gathers evidence instead of "the project's
    evidence script". Set it once in package.json:

    { "evidenceLayer": { "gather": "pnpm evidence --base origin/main" } }

    Without it, the gate names the command the current run recorded, or - for a
    project with an evidence script - <package manager> run evidence, from
    its packageManager field (npm when there is none). When a run exists, the
    base it was gathered against is named too.

  • One fingerprint per stop. Checking a review and a final message used to
    fingerprint the tree once per text, and find the repository and its state
    directory again for each check and each journal write. A stop now resolves
    them once.

  • seal/not-gathered says where the block came from. It names the kept run
    that produced the cited id, when one is still kept, and the id the current
    run gave the same command. A final message citing an unknown id is told the
    id is not in "the current sealed run", not in "this document".

The gatherer

  • Uncommitted and untracked changes are part of the change. The plan (a
    scoped lintCommand, testGate) and GatherResult.changedFiles used to see
    only git diff base...head. They now also get every tracked edit and
    untracked file on top of the reviewed commit, minus the review and the
    bundle. The diff block keeps describing the commits, so its id does not
    move; tracked edits get a ## uncommitted block (git diff --stat HEAD),
    and new files are in the working-tree block. GatherResult.committedFiles
    holds the commits' part alone. The header says how many files are not
    committed.
  • Per-step timeouts. GatherOptions.timeoutMs takes one number for every
    step or one per step name ({ test: 900_000 }); the default stays 300
    seconds. The README's "at your project's own gates and timeouts" is now
    true.
  • The bundle header names the gather command and base - from
    GatherOptions.gatherCommand or package.json, as above - instead of always
    "pnpm evidence", and the seal records both (Seal.gatherCommand,
    Seal.base) for the gate to name.
  • Each artifact block is printed once. A block that backs a suggested claim
    is printed under Suggested claims with that claim; its step's section
    points there. Two steps running one command give one claim.
  • A bundle written to another Windows drive is sealed. With --out on a
    drive other than the repository's, the path was not recognised as outside
    the repository, git refused to leave it out of the fingerprint, and the
    gather ended "not sealed: could not drop the excluded paths". Such a path is
    now left out like any other outside path.

Behaviour changes to check when upgrading

  • A scoped lint or test gate that relied on seeing committed files only now
    also gets uncommitted and untracked ones; committedFiles has the old list.
  • A dirty tree adds a ## uncommitted block to the bundle.
  • A tool that read a step's block out of its own section of the bundle finds
    a pointer there now; the block is under Suggested claims.
  • Sample-integrity quotes are pre-filled for committed files only, since
    receipt checks them at the reviewed commit.
  • summariseGate counts closed-unchecked as closedUnchecked, not among
    degradedAllows.

API

  • New: gatherCommandFor(root, recorded?), GatherOptions.gatherCommand,
    GatherOptions.timeoutMs, GatherResult.committedFiles,
    Seal.gatherCommand, Seal.base, LintOptions.artifactSourceName,
    untaggedBlocks(entries), GateSummary.closedUnchecked, and the gate
    outcome closed-unchecked.
  • sourceFingerprint takes the repository root and readCurrentRun the state
    directory, checkFreshness a fingerprint already taken - each optional, for
    a caller that has it.

Repository

  • The suite is 473 tests across 38 files.

Full Changelog: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 11:11

evidence-layer 0.9.0

Claims in a project's own review format - a risk table's cells, a list - can
now be graded by the same rules as **Claim**: blocks, the distance a grade may
sit below its claim is the caller's to set, and runAsync takes its arguments
as an array. Each removes a workaround a consuming project had to write around
this package. Nothing here changes what an exit code means.

Inline claims

lintClaims(markdown, 'review.md', {
  inlineClaims: [{ line: 14, text: 'store leaks per request VERIFIED[tests-a3f91c: failed=0]' }],
})
  • The caller finds the claims - its table grammar stays its own - and they get
    the address, commit, predicate, outcome and shotgun rules a **Claim**:
    block gets. A bare VERIFIED looks below its line for its block, as a block
    claim's does.
  • An inline claim has no lines below it to borrow: a DOCUMENTED claim cites
    inside its own text, and an INFERRED one carries its reason beside the tag
    ([INFERRED: fewer unique URLs -> more cache reuse]). Two new reason codes:
    claims/reasoned and claims/no-reason.
  • Inline claims count towards claims/shotgun together with the blocks, and a
    document whose only claims are inline is not reported as claims/none.
  • A line outside the document is a RangeError: a finding that points
    nowhere helps nobody.

Where a grade may sit: gradeWindow

  • lintClaims(md, file, { gradeWindow: 5 }) lets the grade sit up to that many
    lines of prose below the **Claim**: line. The default stays 2
    (GRADE_WINDOW); 0 means the claim line only.

runAsync(program, { args })

  • With args, nothing in an argument reaches a shell on POSIX. On Windows,
    where pnpm is a .cmd and needs one, each argument is quoted for cmd.exe;
    one containing " or % cannot be quoted reliably there and rejects the
    promise instead of running as something else.
  • The result's command is the same line on every platform - POSIX-quoted
    where an argument needs it - because artifactId hashes it.
  • Without args, nothing changes.

Expiry dates, written down

  • A governance exception expires, and a decision record goes stale, at 00:00
    UTC on the listed date - the listed day is the first one not covered, in
    every time zone. This was always the behaviour; it is now documented in
    docs/governance-exceptions.md and docs/evidence-layer.md, and the
    boundary is pinned by tests.

Behaviour changes to check when upgrading

  • A fenced block between a claim and its grade no longer counts against the
    window, and cannot supply the grade.
    A grade written below a code example
    used to be out of reach (claims/untagged); it is now found. A grade-like
    line inside a fence used to be taken as the grade; it no longer is.
  • The next **Claim**: line ends the search. An untagged claim directly
    above a tagged one used to take that one's grade; it is now
    claims/untagged, which is what it was.

API

  • New: GRADE_WINDOW, INLINE_REASON_WORDS, InlineClaim;
    LintOptions.gradeWindow and LintOptions.inlineClaims;
    RunAsyncOptions.args. findClaims takes an optional window.
  • Reason codes claims/reasoned and claims/no-reason.

Repository

  • The suite is 453 tests across 37 files.

Full Changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 05:15

evidence-layer 0.8.0

The runtime gate now runs in Codex as well as Claude Code, a gathered test
step records how many tests it ran, and init --review no longer writes a
command a shell could be turned against. Nothing here changes what a review
must contain or what any exit code means.

Security: init --review could write a command that runs code

In 0.5.0 to 0.7.0, evidence-layer init claude --review <paths> put the value
on the hook's command line inside double quotes, which a POSIX shell still
expands. --review '$(touch x)' wrote hooks that ran touch x every time
one of them ran, for everyone who checked the settings out. The value
is an argument to init, so it takes someone - or an agent - running init
with a crafted value, and the result is committed where others run it.

init claude|codex now refuses a --review value with any character other
than letters, digits and . _ / , @ + -: it exits 2 and writes nothing.

What to do: look at the --review part of the evidence-layer commands in
.claude/settings.json (and .claude/settings.local.json). Plain paths are
fine. Anything else - $, a backtick, ;, |, &, a quote - should be
removed: run init again with plain paths, which replaces evidence-layer's
own entries and keeps every other hook.

The rest of the hook path was tested against the same attack and needed no
fix: for both hosts and every event, a payload in every field the agent
writes - a session id, a written path, a patch's paths, a review's claims
and receipt, the final message - and a repository directory itself named
$(...), with nothing run (test/host-injection.test.ts).

The runtime gate in Codex

npx evidence-layer init codex     # writes .codex/hooks.json; approve the hooks in /hooks
  • The same three hooks as Claude Code: session start, before a file write,
    and stop, with the same decisions. DEC-0009
    records why Codex is supported now.
  • Codex edits files with apply_patch, so the guard on the evidence ledger
    reads every path a patch adds, updates, deletes or moves a file to.
  • Codex gives a hook no project directory, so the command finds the package
    from git rev-parse --show-toplevel, as Codex's documentation does; on
    Windows it uses npx --no-install. Codex runs a project's hooks only once
    they are trusted, and again after init changes them.

Work summaries: how much a test step checked

VERIFIED[tests-…: passed>=N] reads a [SUMMARY tests: …] line, and until
now nothing wrote one. DEC-0008:

import { gatherEvidence } from 'evidence-layer'
import { summariseVitest } from 'evidence-layer/adapters/test-reports'

await gatherEvidence({ repo, base, summaries: { test: summariseVitest() } })
  • New entry point evidence-layer/adapters/test-reports: summariseJUnit
    (JUnit XML - pytest, Maven, Gradle, dotnet test, go-junit-report,
    jest-junit), summarisePytest, summariseVitest and summariseNodeTest
    (node --test), and the parsers under them. A JUnit report older than the
    step is refused, so an earlier run's counts are never credited to this one.
  • The bundle's suggested claims then carry the counts:
    VERIFIED[tests-…: exit=0, passed>=3, failed=0].
  • PlanOptions takes testCommand and typecheckCommand, so a project not
    on pnpm can gather. pytest, rspec, phpunit, mypy, pyright, ruff, flake8
    and pylint are recognised for what they are; pytest used to be a plain
    command, held to none of the test rules.

Fixed

  • The GitHub Action ref the README shows (@v0.7.0) would have stayed behind
    at every release. check:docs now holds it to the package version and the
    release rewrites it, so the snippet above names this release.
  • The build's type-resolution check (attw) only warned; it fails now. The
    boundary test now also sees export … from and import().

The example project

examples/plain-node-project is set up the way a project using this package
should be: an evidence script with work summaries, the action in CI, and the
hooks for both hosts. Inside this repository it is a template - git answers
for evidence-layer there - so npm run standalone copies it out as a
repository of its own, where the gate and the seal work on it.

Repository

  • check:ladder runs in CI on every commit a push or pull request brings.
  • The action's download-the-release path runs in CI, through real npx, in
    a workspace with nothing installed.
  • Freshness is tested for deletes, renames, mode changes and symlinks.
  • The suite is 425 tests across 34 files.

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 17:06

evidence-layer 0.7.0

The checks now run in GitHub Actions as one workflow step, and put each
finding on the line it is about. Until now CI meant a run: step and a reader
opening the log to find out why the job was red. The decision is
DEC-0007.
Nothing here changes what a review must contain or what any exit code means.

The GitHub Action

- uses: actions/checkout@v5
- uses: AndrewBogdanovTSS/evidence-layer@v0.7.0
  with:
    claims: review.md            # comma- or newline-separated
    # receipt: review.md         # needs fetch-depth: 0 and the target branch
    # governance: 'true'         # also run governance --enforce
    # strict: 'true'             # warnings and unverifiable fail too
  • Every finding that is not a pass becomes an annotation on its line, titled
    by its reason code,
    and the job summary lists them. unverifiable and flaky are warnings
    whose title says which, so "nobody could check this" does not read as a nit.
  • It runs the evidence-layer the project installed, so CI and a laptop give
    the same verdict. With none installed, it runs the exact version the action
    was released with - @v0.7.0 runs 0.7.0, next year too - or the version
    input; never latest.
  • Outputs exit-code and conclusion (success, failure, flaky,
    misconfigured). Nothing to check, a missing review file, an installed
    package too old to annotate, or a version that is not exact is
    misconfigured, exit 2, and outranks a failure - a step that could not run
    never passes.
  • No token and no permissions: annotations are workflow commands, so they land
    on the job's own check run with nothing to grant.
  • A plain Node script with no dependencies and no build, so the action runs
    straight from this repository at the tag a workflow names.

--format github

  • claims, receipt and governance take --format github for any run:
    step: the annotations, then the usual text report, then a job summary when
    GITHUB_STEP_SUMMARY is set. The exit code is the text mode's.
  • A review cannot give the runner instructions. It is text written by an
    agent or by the author of a pull request from a fork, and the runner obeys
    any stdout line that reads like ::add-mask::.... Annotation values are
    escaped the way the runner un-escapes them, so a newline cannot start a
    second command, and the text report - which quotes the review - runs under
    ::stop-commands:: with a token chosen at random for each run.

Publishing

  • Releases are published through npm's Trusted Publisher: the workflow's OIDC
    token is the only credential, and there is no long-lived npm token to leak
    or renew. 0.6.0 was the first release published this way; with the token
    gone, a broken Trusted Publisher fails the publish rather than falling back.

API

  • New exports: annotations, escapeData, escapeProperty, stopToken and
    renderFindingsSummary; printFindings, which every check uses to print in
    the format asked for. Format gains 'github'.
  • package.json declares "evidenceLayer": { "outputFormats": ["text", "json", "github"] } alongside hookProtocol - the capability the action
    checks instead of comparing versions.

Repository

  • CI runs the action on a fixture review with uses: ./, so each commit's
    action and checks are tested together.
  • The suite is 377 tests across 29 files.

Full Changelog: v0.6.0...v0.7.0

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 15:39

evidence-layer 0.6.0

Every finding now says which rule produced it, in a word a program can match,
and the checks can print their findings as JSON. Until now a tool that wanted
to act on a finding had the level - how bad, not what - or the English
sentence, which changes whenever someone improves it. The decision is
DEC-0006.
Nothing here changes what a review must contain or what any exit code means.

Reason codes

  • Every finding from claims, receipt, claims --seal and governance
    carries a code naming the rule behind it: claims/predicate-unmet,
    claims/unknown-artifact, receipt/wrong-base, seal/stale,
    governance/unreached and 65 more. The full list, with what each means, is
    in docs/evidence-layer.md.
  • A code names a rule, not an outcome. The five levels are unchanged; a rule
    that can pass or fail has one code for the pass and one for each way it
    fails, so the code alone says what went wrong.
  • A released code keeps its meaning. A rule whose meaning changes gets a new
    code; the suite fails if a released one is removed or renamed. claim and
    detail remain prose and may be reworded in any release.
  • The package's own checks cannot produce a finding without a registered code:
    it is a type error. Finding.code stays optional for a project's own checks.

--format json

npx evidence-layer claims review.md --format json
npx evidence-layer receipt review.md --format json
npx evidence-layer governance --format json
  • Stdout is one JSON document and nothing else - no text, no delimiters - and
    the exit code is the one the text mode gives.
  • The document is the report governance already printed between its
    ---GOVERNANCE-JSON-BEGIN--- delimiters, now schema 4: each finding has
    code, and the report has check (claims, receipt, governance). Both
    fields are additions; a reader of schema 3 still reads it.
  • An unknown format is bad usage (exit 2), never a quiet fall back to text.

The runtime gate runs in this repository

  • evidence-layer init claude, run in evidence-layer's own repository, wires
    the plugin's launcher instead of node_modules/evidence-layer, and the
    launcher treats a directory whose own package.json is evidence-layer as
    the package. A checkout that has not been built says the gate is inactive
    and suggests pnpm build, rather than failing the hook. The repository now
    commits its .claude/settings.json, so every Claude Code session in it runs
    the gate.

Fixes

  • A path:0 citation reported "resolves" while failing. It now says lines are
    numbered from 1.

API

  • New exports: CODES, Code, CodedFinding; formatOf and Format;
    printJsonReport. buildReport takes an optional check.
  • lintClaims, checkReceipt, checkProvenance, checkFreshness,
    checkReachability, checkExceptions and checkDecisionHygiene return
    CodedFinding - a Finding with code set, so existing callers compile
    unchanged.
  • REPORT_SCHEMA is 4.

Repository

  • AGENTS.md is the source of truth for contributors, people and agents;
    there is deliberately no CLAUDE.md, because Claude Code reads AGENTS.md
    when none exists.
  • The suite is 365 tests across 27 files.

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 14:20
ecbc844

evidence-layer 0.5.0

Until now every check ran in CI, after the change and its account were
written - and usually after someone had started reading them. 0.5.0 adds the
moment before that: a runtime gate that checks an agent's claims when it tries
to stop, so the agent that wrote "the suite passes" is the one that fixes it.
It also closes a shell-injection hole in receipt that every earlier version
has. The decision, its threat model and its invariants are
DEC-0005;
why each choice was made is in the
implementation report.

Security

  • receipt no longer runs anything a review says. It built git commands by
    concatenating the receipt's Target branch and every Sample integrity path
    into a shell string, so a review containing $(...) executed it on the
    machine checking it - in CI, a pull request from a fork. Git is now called
    with an argument array and no shell (git() / gitCapture() in the API),
    gatherEvidence passes caller-supplied revisions after --end-of-options,
    and changed paths are read NUL-separated. Upgrade if you run receipt on
    reviews you did not write.
    There will be no 0.4.x patch: the fix is in
    0.5.0 and later only.

Predicates: a claim names the numbers its artifact must show

**Claim**: the suite runs 71 passing tests.
**Grounding**: VERIFIED[tests-a3f91c: exit=0, passed>=71, failed=0]
  • Keys are a closed set read off the cited block itself - exit, stopped
    (none / timeout / overflow / killed), and the work-summary counts
    (passed failed skipped todo, files ignored errors warnings) - so a
    predicate checks identically locally and in CI.
  • Nothing is dropped: an unknown key, a fragment that does not parse, a key
    given twice, a count the artifact does not carry, a non-decimal or unsafe
    number, or an ordering comparison on stopped is an error.
  • A VERIFIED[...] bracket that is not an address is an error, instead of
    silently falling back to the legacy bare-VERIFIED proximity check.
  • Predicates are checked in addition to the existing outcome rules, never
    instead of them, and the tag's text is removed before a claim's wording is
    read - passed>=3 inside a failure claim no longer reads as success.
  • Two different artifact blocks under one id are now an error (identical
    copies are fine). Previously the second was never looked at.
  • The skeleton writes the predicates for you, read off the run it gathered.
  • New negative fixture: examples/plain-node-project/reviews/predicate-mismatch.md.

The seal: was this block gathered, and is it still about this code?

  • Every gatherEvidence() now publishes a sealed run into the checkout's
    git directory (git rev-parse --git-path evidence-layer): the bundle, a
    canonical hash of every artifact block, and a fingerprint of the working
    directory's content - a git tree id computed through a throwaway index, so
    the real index is never touched. Committing unchanged content does not move
    it; writing the review does not either; editing any other non-ignored file
    does.
  • A run becomes current only once it is complete. A gather that fails - or
    whose own steps write into the files they check, like a formatter or code
    generator - is never published; the bundle names the step and the paths, and
    GatherResult reports sealed, runId, treeChanged and coverage.
  • evidence-layer claims review.md --seal adds two checks: every cited block
    must be one the current run produced, unedited, and the code must still be
    what that run saw. Editing a file after gathering is an error that names the
    file. An untracked file too large to hash, a sparse checkout or a split index
    makes freshness unverifiable - never a pass.
  • gatherEvidence({ seal: false }) turns it off; reviewPaths names the
    review files to leave out of the fingerprint (default review.md).
  • Not an attestation: it catches the default failure, not a writer who means to
    fake it. CI, gathering its own evidence, stays the independent party.

The runtime gate, for Claude Code

  • Session start tells the agent the claim grammar and snapshots the review
    files, so a later stop checks only what this session wrote.

  • Before each file write, a write into the gate's own ledger is refused -
    an accidental-write guard, not a sandbox.

  • At stop, claims, receipt, provenance and freshness run over every
    review file the session changed, and over the **Claim**: lines of the
    agent's final reply, resolved against the sealed bundle. Ordinary prose is
    not graded. Errors block the stop, with the findings as the reason.

  • A retry episode blocks at most twice, then lets the agent through with
    the errors attached; only a clean check closes it. The gate fails open by
    default; --fail-closed, --keep-blocking and --strict are the stricter
    trades.

  • Every decision goes into a journal in the checkout's git directory. An allow
    that checked nothing is never recorded as a pass.

  • Install with npx evidence-layer init claude (merges into
    .claude/settings.json, or --local for settings.local.json; keeps every
    hook that is not its own; idempotent), or as a plugin:

    /plugin marketplace add AndrewBogdanovTSS/evidence-layer
    /plugin install evidence-layer@evidence-layer
    
  • evidence-layer hook claude <event> always exits 0 and answers in Claude
    Code's own protocol, because Claude Code reads an exit code as a decision and
    this package's 2 (bad usage) is Claude Code's "block".

  • The pre-write hook loads only what it needs (~55 ms on the development
    machine); session start and stop measured 70-100 ms. In a live Claude Code
    2.1.302 session the gate blocked an untagged claim, the agent fixed it, and
    the next stop went through.

New commands and options

  • evidence-layer hook <host> <event> - the gate, as a host's hook runs it.
  • evidence-layer init claude [--local] [--dry-run] - set the gate up.
  • evidence-layer claims --seal [--repo <path>] - hold cited blocks to the
    sealed run.
  • evidence-layer journal report --include-gate - the gate's own counts:
    blocks (tagged real or false), clean stops, stops let through without a clean
    check, refused writes, median decision time.
  • evidence-layer journal tag <id> real|false --gate - tag a gate block.

API

  • New exports: parsePredicates, evaluatePredicates, suggestPredicates;
    sourceFingerprint, publishRun, readCurrentRun, checkProvenance,
    checkFreshness, canonicalBlock and the rest of seal; decide and the
    gate types; git and gitCapture.
  • lintClaims options: artifactSource (resolve ids against another
    document, such as a sealed bundle) and prose: false (grade only **Claim**:
    lines).
  • package.json declares "evidenceLayer": { "hookProtocol": 1 }, which the
    plugin's launcher checks instead of comparing version strings.

Performance

  • The CLI loads each command only when it is the one asked for. Smoke prints
    the median cold start against a 100 ms budget (~51 ms on the development
    machine), as a number rather than a gate - timing on shared runners is noise
    as often as signal.

Behaviour changes to check when upgrading

  • gatherEvidence writes into the repository's git directory by default. Pass
    seal: false if that is unwanted.
  • A VERIFIED[...] bracket that is not a valid address used to pass as bare
    VERIFIED with a warning; it is now an error.
  • Two different blocks under one artifact id in one review are now an error.

Governance

  • DEC-0005
    records eight invariants, each enforced by pnpm test or pnpm check:docs.
  • check:docs now holds the README's supported-hosts table to an adapter with
    recorded fixtures.
  • The boundary test covers src/hosts/: it imports only builtins, the core and
    its own contract, and the core never imports it.
  • The suite is 356 tests across 26 files.

Full Changelog: v0.4.0...v0.5.0