Skip to content

[DO-NOT-MERGE] trial branch: all skill PRs merged, for install and testing - #108

Draft
MajorLift wants to merge 241 commits into
mainfrom
jongsun/trial/combined
Draft

[DO-NOT-MERGE] trial branch: all skill PRs merged, for install and testing#108
MajorLift wants to merge 241 commits into
mainfrom
jongsun/trial/combined

Conversation

@MajorLift

@MajorLift MajorLift commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Caution

DO NOT MERGE. This branch is a merge of fourteen open PRs, kept on MetaMask/skills so the
whole set can be installed and exercised as one thing. It has no content of its own — every line
in it belongs to one of the PRs below and should be reviewed there. It will be deleted once they
land.

Why this exists

No single PR installs into a working state. mms-evidence alone routes to engines that live in
eight different domains, so checking out its branch gets you an orchestrator naming skills that
are not installed. Reproducing the full set by hand means a fourteen-way merge against two
remotes, which is not a reasonable thing to ask of someone who wants to spend twenty minutes
trying it.

What is merged

39 skills across 13 PRs, plus one CI change. --no-ff on top of main.

PR branch skills
#84 jongsun/add/pr-validate-falsifying-test evidence
#106 jongsun/add/audit-skills falsifiers-first, unmeasured-join, instrument-check, scope-of-search, coverage-partition, selection-audit, silent-failure, unintended-breakage, red-on-base, distinguishing-observation, observability-gap
#107 jongsun/add/lane-graphs lane-graphs
#98 jongsun/add/debug-orchestrator debug
#81 jongsun/add/memory-leak-hunt-skill-v2 memory-leak
#97 jongsun/add/race-condition-proof race-condition-repro
#83 jongsun/add/security-domain supply-chain-audit, lavamoat-policy
#90 jongsun/add/privacy-egress-diligence privacy-egress-diligence
#43 add/performance (fork) react-render-delta, selector-antipattern-scan, effect-antipattern-scan, extension-profiling, data-analysis, benchmark-design
#76 add/analytics-tracing-cross-ref (fork) sentry-quota, sentry-mcp-queries, sentry-grafana-correlation, grafana-tempo-queries, performance-attribution, instrumentation
#69 add/typescript-typing-skill (fork) tsc-blindspots, avoid-any, derive-types, decompose-large-files, migration-context-cost
#44 add/platform-skills (fork) extension-errors-debugging, extension-lifecycle-decoupling
#91 jongsun/add/agent-run-cost agent-run-cost
#99 jongsun/ci/ship-hooks-and-trigger-check — ships hooks/, adds the installed-command check

One conflict along the way, in .github/CODEOWNERS, resolved by keeping both the
/domains/security/ and /domains/stability/ lines.

Deliberately not merged: #79 (mobile-visual-testing), which is someone else's open PR, and
the CI/infra PRs #46, #87, #93, #94, #95, #103, which change tooling rather than add skills. Say
so if you want any of them in.

Install

git clone https://github.com/MetaMask/skills.git ~/src/mm-skills
cd ~/src/mm-skills && git checkout jongsun/trial/combined

METAMASK_SKILLS_DIR=~/src/mm-skills \
  node ~/src/mm-skills/bin/metamask-skills.mjs sync \
  --target /path/to/metamask-extension \
  --maturity experimental --save

Add --dry-run first to see the file list without writing. That installs 65 skills for
metamask-extension (70 for metamask-mobile, 43 for core) into .claude/skills/mms-*/,
.cursor/rules/ and .agents/skills/. --save writes the selection to .skills.local so later
runs repeat it.

Four things that are easy to get wrong:

  • Don't pass --domain. --domain pr-workflow looks correct and silently drops
    red-on-base, memory-leak, react-render-delta and the rest, while mms-evidence still
    names them. The skills span fifteen domains: pr-workflow (12), performance (9), testing
    (8), analytics (6), typescript (5), coding (5), web3-tools (4), perps (4), security
    (3), stability (2), platform (2), general (2), ui (1), swaps (1), agentic (1).
  • --maturity experimental is required. Everything except lane-graphs is stable, and the
    default filter is stable — so a default run installs 64 of 65 and does not report which one it
    skipped.
  • The consumer repo name changes the set. It is inferred from the target's package.json;
    override with --repo if there isn't one.
  • These are scope: project. They install into <target>/.claude/skills/, not $HOME.
    --target defaults to the current directory, so run it from — or point it at — the repo you
    want them in.

To remove them: --prune-stale on a later sync, or delete the mms-* directories.

Usage

Two orchestrators, and the rest are engines and lenses they route to. Everything also stands
alone, triggered by name or by description match — you do not have to route by hand.

mms-evidence takes a claim about a PR and produces something a reader can check without
trusting the author:

/mms-evidence 45123
/mms-evidence plan 45123      # what would be measured, before measuring it
/mms-evidence lane render     # route to one measurement kind

It runs three ways: on a PR whose claim someone else made, in the inner loop against uncommitted
changes before a reviewer sees them, and on a symptom with no claim yet, where the hypothesis to
kill is your own.

mms-debug is the same shape pointed at a symptom rather than a claim.

Representative engines: /mms-red-on-base for a regression test that must fail on the base
commit; /mms-silent-failure for paths that can fail without saying so; /mms-unmeasured-join
for the inference step between two true facts that nobody instrumented; /mms-tsc-blindspots for
what the type checker does not catch; /mms-sentry-mcp-queries and
/mms-sentry-grafana-correlation for reading production signal; /mms-supply-chain-audit and
/mms-privacy-egress-diligence for dependency and egress review.

What it is for. The bar is not that a run reaches the right verdict on every concern. It is
that it surfaces concerns, falsifiers and avenues of inquiry that a human then judges. A run that
ends "inconclusive, and here is what would settle it" has done its job.

Known state

Six lint errors, all pre-existing on the source branches rather than introduced by the merge —
main is clean under the same linter. Five are the same class, from the check #99 adds: a
description advertises /name where the installer emits mms-name. Those branches predate the
rule and only fail once combined with it.

skill error
performance/react-render-delta description names /react-render-delta, installed as mms-react-render-delta
stability/memory-leak same class
stability/race-condition-repro same class
typescript/tsc-blindspots same class
pr-workflow/debug same class
pr-workflow/evidence probes/ is not an allowed directory beside skill.md

They are fixable only on the PRs that own them. Fixing them here would put the fix on a branch
that is never merged and leave the real one broken — which is the failure mode #84 was already
caught by once, when four runner fixes landed on the copy CI executed and never reached the copy
under review.

Also carried here: domains/pr-workflow/skills/evidence/scripts/ duplicates
a standalone runners repository (v0.1.0). That
repository is the source; the copies are pending removal and exist so the branch can run
standalone while it is reviewed. evidence-runners/tools/check-duplicates.sh compares them.

MajorLift added 30 commits June 5, 2026 14:52
sentry-mcp-queries: document data-fidelity loss on older releases for
longer-range (30D+) queries (sample-rate drift, extrapolation hiding thin
samples, retention downsampling) and percentile (p75+) sample-size/quality
filtering; add table rows + pitfalls for stored-span-count and
superseded-patch releases.

performance-attribution: new skill for attributing release-over-release
p75/p95 movements to code changes via black-box diff analysis, with an
extension repo file (Trace Explorer queries, key transactions,
highest-sample-patch version selection, 90d-vs-30d empirics, hot-path
files, core-package changelog analysis, worked v13.11->v13.15 catalogue).

analytics-instrumentation: cross-link volume-estimation caveats to the
new fidelity guidance. CHANGELOG updated.
…n-out, latent trace? param, trace-arg PR-review scan, per-name budget
…ary-identification is step one of any migration
…und unit conversion, swap in the stronger derive-types example, slim CHANGELOG

- migration-context-cost: line count is a factor not a non-factor; fan-in is a reading cost (not a change/review surface), fan-out is the change surface; drop the wrong "upstream types land first" and off-topic barrel bullet
- decompose-large-files: the point is converting to TS in small self-contained units, not extraction
- derive-types: replace the NetworkState restatement with the reinvented-messenger + hand-copied-return example (derive via `ReturnType<Action['handler']>`)
- CHANGELOG: list the domain, not each skill
Two-phase retention review for JavaScript/TypeScript.

Phase 1 is a static read of a diff: enumerate the retention primitives the change
introduces — listeners, timers, pending-request registries, subscriptions, module
singletons, growing collections — and pair every acquire with its release site.
A primitive with a teardown is safe; one without is the finding.

Phase 2 escalates to DevTools/CDP heap snapshots only for a primitive the read
cannot pair. Leading with the read rather than the instrument settles most leak
claims without ever taking a snapshot.
…oat-policy-diligence

Two composing skills for "is this dependency change safe to take".

`supply-chain-audit` is the breadth pass: Socket findings, `yarn npm audit`
advisories, lockfile and manifest diffs, and the fronts no upstream scanner sees
because they are things the repo does to its dependencies afterwards — yarn
patches that modify dependency source at install, `resolutions` that force or
stub versions, `npmAuditIgnoreAdvisories` suppression lists, CI actions riding
mutable tags, and yarn plugins that execute at install.

`lavamoat-policy-diligence` is the depth pass it delegates capability
containment to. Because a LavaMoat policy is generated from a real run, every
grant has a call site by construction — so "each addition is justified" is a
tautology, not a finding. It instead reads each grant's use at the installed
version to find its gate, and sorts into removable / removable-at-a-cost /
load-bearing.

Neither renders an accept/reject verdict; disposition belongs to the people who
own the dependency.

Adds a CODEOWNERS entry for the new domain, defaulted to the platform teams.
Replaces #56, whose skill.md shipped frontmatter with no body at all — the
installer's bodyAfterFrontmatter() returned empty, so an agent loading it got a
description and six references it had no instruction to read.

pr-validate: for a PR's specific falsifiable claim, name the observation that
would prove the claim false, gather it, and publish it into the PR body. Drives
the AEP harness (visual_validation, perf_validation) as the primary engine,
backed by a catalog of complementary lanes, a trustworthiness gate that rejects
vacuous passes, and a publishing flow with an audience-reachability rule for
re-hosted artifacts.

falsifying-test: the strongest single proof that a fix targets the reported bug
— a test that fails on the base commit and passes on the branch, with both runs
shown. Its own falsifier is a base-commit failure for the wrong reason (import
error, missing fixture, unrelated red), which looks identical in an exit code
and proves nothing. pr-validate calls it as the engine behind lane B3.

hooks/pr-evidence-gate.py enforces the trustworthiness gate at emit time,
blocking an outward-facing write whose body carries an unbacked verdict, an
untracked deferral, a CI restatement, a bare or truncated identifier, a mutable
ref, a dump-as-resolver, a link-only or data-only exhibit, or a step waiver. It
polices `gh api` body writes as well as the porcelain, since a PATCH to a
comment is the same publish with a different spelling.
CHANGELOG.md tracks consumer-facing changes to the `@metamask/skills` package,
per CONTRIBUTING's "CLI / tooling changes" section. No merged skill-only PR adds
an entry (#80, #78, #70, #62, #61 all touch zero changelog lines).

Carrying one here bought nothing and was the sole source of this branch's
conflict with `main`, since every skill PR edits the same `[Unreleased]` block.
Restoring the file to its merge-base state makes the branch conflict-free
without a merge commit.
CHANGELOG.md tracks consumer-facing changes to the `@metamask/skills` package,
per CONTRIBUTING's "CLI / tooling changes" section. No merged skill-only PR adds
an entry (#80, #78, #70, #62, #61 all touch zero changelog lines).

It was also the sole source of this branch's conflict with `main`, since every
skill PR edits the same `[Unreleased]` block.
CHANGELOG.md tracks consumer-facing changes to the `@metamask/skills` package,
per CONTRIBUTING's "CLI / tooling changes" section. No merged skill-only PR adds
an entry (#80, #78, #70, #62, #61 all touch zero changelog lines).

It was also the sole source of this branch's conflict with `main`, since every
skill PR edits the same `[Unreleased]` block.
The selector and effect anti-pattern definitions existed in two places: these
knowledge files, and the `performance` skill's own mm-* references already on
main. Same patterns, same worked examples, two homes that would drift.

knowledge/selector-anti-patterns.md and knowledge/effect-anti-patterns.md are
now the canonical, platform-agnostic taxonomy — the union of both sides. The
selector file absorbs mutation-in-result and over-broad-input from
mm-selector-memoization; the effect file absorbs the dependency-side patterns
from mm-hook-dependency-arrays and the lifecycle-side patterns (derived state,
effect chains, uncancelled async).

mm-selector-memoization.md keeps everything only it can say — the codebase's own
selector creators, the verified instance table with file:line, the fix recipes,
the scoped greps, the don't-over-correct caveats — and maps each generic pattern
onto this codebase instead of redefining it. mm-hook-dependency-arrays.md keeps
its richer JSON.stringify treatment and gains a scope note.

Citations are by NAME, not by relative link. `install` copies domain knowledge/
and a skill's references/ as siblings under the installed skill directory, so
`../../../knowledge/x.md` resolves in the repo and breaks once installed, and
`../knowledge/x.md` does the reverse. Section anchors are dropped for the same
reason — they broke the moment the taxonomy was renumbered.

Also drops the CHANGELOG entry: that file tracks the @metamask/skills CLI
package, no merged skill-only PR adds one, and it was this branch's sole
conflict with main.
Folds in the react-render-proof skill (was #82) and the mobile reference-library
additions (was #49). All three were the same effort seen from different ends —
moving performance work earlier in the loop — and they share a substrate, so
reviewing them apart meant reviewing the substrate three times.

The loop this domain now covers:
- catch it at review    — effect/selector anti-pattern review skills, driven by
                          the knowledge taxonomy
- prove it moved        — react-render-proof, with a delivery gate so an arm
                          whose treatment never reached the bundle cannot report
                          as a null
- measure it honestly   — data-analysis, benchmark hygiene, web-vitals framing
- know the codebase     — the mm-* reference library and its audit playbook

Also neutralizes five references to private planning tickets, which do not
belong on a public repository — they named internal epic and audit-ticket
numbers. The surrounding guidance is unchanged; only the identifiers are gone.
These name MetaMask-org planning epics and audit tickets. The audience for this
repo is the MetaMask org, for whom those identifiers are load-bearing context —
they are where the guidance came from and where the follow-up lives.

The scrub line is personal references, not org-internal ones.
Over-scrubbed. The audience is the MetaMask org, and the ticket is the evidence
for the claim the item makes — that the LaunchDarkly provisioning blocker covers
only the prod-flag half of that lane. Without it the example is an assertion.

The scrub line is personal references, not org-internal ones.
The skill covers runtime retention behaviour, not code authoring, and `coding`
reads as language- and style-level guidance. Registers `/domains/stability/` in
CODEOWNERS alongside the other platform-owned domains.
…main

Was a separate PR against `domains/testing`. It belongs here: its subject is
whether a hand-written type agrees with its authoritative source, which is the
question `derive-types` answers from the authoring side, and it shares this
domain's premise that a green `tsc` is not evidence the types are correct.

Directory name and frontmatter `name` already agree; only the domain moved.
Renamed on the security-domain branch; installs as `mms-lavamoat-policy`.
A diligence comment renders no verdict and deliberately does not use the Validation Run
envelope. That exemption meant it was checked by nothing: this gate only knew the
Validation Run shape, and `pr-evidence-gate.py` by design does not trip on a body
claiming no verdict. So every rule the diligence skills state about their own output —
including "runtime claims need a runtime artifact" — had no execution path.

It showed. A lavamoat comment shipped with no marker pair, an `npm pack` specifier set
no reader could fetch, and two bare integers traceable to nothing.

`--diligence` swaps the four envelope checks for that contract's own — its marker pair,
its header, permalinks pinned to a tag or SHA rather than a branch head, and a runtime
claim check asking for the thing a `/blob/` link cannot witness. 3, 8 and 9 report SKIP
with the reason rather than passing silently, since a check that cannot fail should not
read as a check that passed. Everything downstream of the envelope is shared, because
those defects are shared.

Run against the comment that prompted this, it fails 1, 5 and 13 and passes the rest.
This repository is public. Naming a private repository here discloses its existence,
its owner and roughly its contents to every reader — and a prohibition naming it
("do not re-host to X, it is private") discloses exactly as much as a recommendation
would. Four such references were doing that, and the guidance survives without them:
the rule is audience-reachability, which is stated directly rather than by example.

Two memory-file citations offered as "source of truth" pointed into a private repo, so
a reader was told to follow a rule whose justification they cannot open. The reasoning
is inlined; the pointer is gone.

The publish-surface snippet hardcoded a GitHub username, which decided the destination
for whoever ran it. Now derived from `gh api user --jq .login`, and the surrounding
prose is second-person rather than first — a shared skill has no "my PRs".

`/attest` is no longer linked to a personal repository. That leaves it named but not
resolvable, which is honest and is the smaller problem; the workflow depending on a
command nobody else has is tracked separately.
The re-hosting section named a personal S3 bucket, its region, its prefix layout, and
stated that anonymous `GetObject` is enabled under `public/*`. That is a live
unauthenticated endpoint advertised, with its structure, to every reader of a public
repository — a larger disclosure than the repository names removed alongside it, and
one that reads as configuration rather than as a secret, which is why it survived two
passes over this file.

Now `EVIDENCE_BUCKET` and `EVIDENCE_REGION` from the environment. The requirements the
bucket must satisfy — anonymous GetObject under `public/*`, listing disabled — are
stated, because those are the load-bearing part; the name never was.

The G5 lane likewise named a private test fork, which carried both the org and a
personal handle. Now "your own test fork".
…setup

Removing the hardcoded username left `gh pr view --jq --arg me "$ME"`, which is not a
thing gh supports — its built-in filter takes no --arg and the command dies with
"accepts at most 1 arg(s)". Piped to real jq instead, and checked against both branches
of the logic: a PR authored by someone else resolves to "skip", one authored by the
caller to "body".

Replacing the named bucket with `EVIDENCE_BUCKET` removed a working default and put
nothing in its place, so the section told you to configure a bucket without saying what
"conforming" meant. The policy is now stated: anonymous `s3:GetObject` under `public/*`,
public-access blocks off for that bucket, `s3:ListBucket` to nobody. With a note that an
org-owned bucket beats a personal one, since artifact links outlive their publisher.
A results section reached a public PR whose entire content was hand-typed to look like
terminal output. Three independent things had to hold for that, and all three did.

The gate is model-invoked, so it can be skipped: the publish and the gate ran as two
statements rather than one chain, and the verdict was read after the write.

The hook that fires on the publish call carried a SECOND, narrower copy of the rules —
keyed on verdict tokens — so a comment rendering no verdict satisfied neither copy.
Two rule sets means the weaker one governs whatever falls between them. The hook now
delegates to `attest-gate.sh`: one rule set, invoked by construction rather than by
choice, in the mode the body's markers imply. It fails CLOSED once it has identified a
body it is about to publish — an enforcement point that waves things through when it
cannot find its rules is not one.

And check 5 in `--diligence` had been rewritten as a phrase denylist ("npm pack",
"complete specifier set"), which is precisely the regression its own comment records as
having shipped four times: every property of plaintext is forgeable by whatever emits
the plaintext. It is a medium test again — if the artifact shows a command or a run
result, it owes the reader something fetchable. `/blob/` links are excluded, because a
permalink to a `.json` file satisfied a naive extension test and was the specific reason
the hand-typed section passed.

Comment-update URLs carry the comment id, not the issue's, so check 12 was asking
whether pull #5177261620 was open. Resolved through the API instead.

Four-arm verified: blocks the exact command and body that shipped; ignores `ls`; ignores
a `gh` read with no body write; refuses when the gate is unreachable.
`mms-evidence` named `/attest` as the gate its output passes and shipped no such thing.
Anyone installing the skill got phase 0 as a script and a reference to a command only its
author had, which is half a publish path for everyone else.

Phase 0 is `attest-gate.sh`, already here. Phase 1 is three briefs sent to fresh
instances — frame, coverage, and how it reads to a stranger — written out in
`references/dispatched-passes.md` so dispatch does not depend on commands that live
elsewhere. The check table in `references/phase-0-checks.md` is generated from the gate
rather than retyped, so it cannot drift from what runs.

Two things the port makes explicit that the original left to discipline. The gate must be
the same shell chain as the publish, because running both and reading the verdict after
the write is how a blocked artifact reached a public PR. And softening a check to fit the
case in hand is called out as an anti-pattern: if the new version could be satisfied by
better prose alone, it is no longer the check.
Three copies of this hook were on one machine and the oldest was the one wired into
settings — no `gh api` matcher, no delegation to `attest-gate.sh`. Every publish through
that path went ungated, and nothing noticed, because a gate that blocks nothing looks
exactly like a gate with nothing to block.

`gate-controls.sh` is the thing that would have noticed. Six arms: three publish routes
that must block, three inputs that must pass. It copies the hook somewhere with no sibling
`scripts/` so `_find_gate()` resolves the way it does in production rather than the way it
does in a checkout — the difference matters, and testing the checkout copy is how the
deployed one stayed broken.

Both halves earned their place immediately. The negative arm caught that delegating to
attest-gate ran it over EVERY published body, so an ordinary reply was judged as a failed
validation run; wiring that would have blocked every normal comment. The gate now applies
only to bodies carrying an artifact marker or a verdict line.

And the positive arm caught itself: the enrichment probe was a single sentence, which that
rule correctly ignores, so the arm had been passing because attest-gate blocked the body
for an unrelated reason. A probe that fires for the wrong reason reports a working rule.

The `enrichment` class is ported forward from the older copy, with the constant it depends
on — it existed in the deployed version and in neither newer one.
The six arms prove the script blocks what it should. They say nothing about whether
anything calls it, and those are different questions — a hook that is unwired, or wired
to a path that no longer exists, is indistinguishable from a hook with nothing to block.
One session ran start to finish with every PreToolUse hook inert: 306 certification
markers written, none enforcing anything, and the ritual read as compliance.

The check enumerates config roots rather than trusting `$HOME`. Its first version did
trust it, found one settings file, reported it as "the" wiring and never looked at the
second — because `$HOME` here points at a per-account directory rather than the login
home. That is the same defect one level up, caught only because two configs were known
to exist and one was missing from the output.

It stops short of claiming liveness, and says so: a settings file naming an existing
file is not proof the running session loaded it. Only a command the gate must block,
issued in a session and observed to be blocked, shows that.
This is the bypass. The hook reads the command as text, so `--body-file $DIR/comment.md`
resolves to nothing — and the code called that "can't read it -> nothing to scan" and
allowed the write. Every publish in one long session used a shell variable for the path,
so every one of them went ungated, including a comment whose entire results section was
hand-typed to look like terminal output. That same body, passed by literal path, is blocked.

An unreadable body is not an absent risk. By that point the command is already identified
as an outward-facing write; not knowing what it carries is the reason to stop.

Also rejects a body argument carrying `$` or a backtick — `--body "$(cat f)"` extracts the
literal characters, scans clean, and publishes whatever the shell substitutes afterwards.

The check reads the command ARGUMENT, not the body. A first version scanned body text for
shell metacharacters and blocked every evidence comment ever written, because markdown
inline code is backticks and these artifacts are full of them. Caught by the negative arm,
which is the half of a control matrix that earns its place on days like this.

Three arms added, verified end to end: the command that slipped through minutes earlier is
now stopped by the deployed hook.
This branch adds `hooks/pr-evidence-gate.py` and `hooks/gate-controls.sh`, and on main
neither would have survived: `hooks` is absent from `BUNDLE_DIRS`, so the linter rejects
the directory outright, and absent from the bundle loop in `tools/install`, so nothing
would copy it even past lint. The gate would have merged into the repository and installed
to nobody.

`hooks` now appears in both.

The third change is the one worth keeping. The two lists are a single contract written
twice, in JavaScript and in Bash, and the schema's own comment asked humans to keep them
in sync by hand. `tools/check-bundle-contract.sh` asserts it instead. It builds a fixture
skill carrying one file in every declared bundle directory, runs a dry-run install from a
copy of the installer placed inside that tree, and checks each directory is reported as
copied. It watches behaviour rather than reading the loop, because a regex over the source
is the same class of evidence as the defect: two declarations agreeing with each other and
neither of them being what runs.

Controlled both ways. It passes as committed. Removing `hooks` from the loop makes it name
`hooks` and exit 1; removing three makes it name three.

Also relocates `probes/metametrics-context.test.tsx`, which lint rejected as an unexpected
directory, to `assets/` where a copy-out template belongs, and points the C4 lane at it.
It had been shipping with nothing in the skill naming it, which is the same defect one
layer over: present, and unreachable.
A check nobody invokes is a check that fires once, on the day it was written. This one
guards a contract that drifts silently: a bundle directory declared in `BUNDLE_DIRS` and
missing from the install loop reaches no consuming repo, and nothing errors, because the
skill still installs, still loads, and still answers without whatever that directory held.

It runs in the lint job, after `yarn lint` and before the changelog steps. It leaves no
files behind, so the job's clean-working-directory gate is unaffected.
tools/install copies source over the installed tree with `rm -rf` followed by
`cp -R`, so an edit made in the installed copy is destroyed on the next sync
with no warning and no record.

Comparing source against the install cannot tell the two cases apart, because
both make them differ: the install was edited locally, or the source moved
ahead. A guard that refuses on any difference breaks every routine update,
which is a good way to get itself disabled. So each install now records a
content hash of what it wrote, and a later run compares the install against
that record. Matching means nothing local happened, and the copy proceeds no
matter how far source has moved.

Bundles with no record predate the manifest and fall back to comparing against
source. That is conservative — it refuses a legitimate update once, until a
manifest exists — on the grounds that a one-time re-run beats a silent delete.

Also adds hooks/ to BUNDLE_DIRS and to the directories tools/install copies.
It was in neither, so a skill shipping a hook had it installed by hand and
tracked nowhere. That is how the evidence skill's wired PreToolUse gate — the
one control that fires on every publish — came to have no source of record.
The existing lint-skill-entry test asserts these two lists agree, and caught
the omission when only one was updated.

Controls, against a temp target and temp HOME:

  clean install                exit 0
  re-install, nothing touched  exit 0
  source moves ahead           exit 0, change propagates
  local edit to the install    exit 1, REFUSED, sha unchanged
  same with --force            exit 0, edit discarded

`node --test test/*.test.mjs` 61/61.
The ARTIFACT and OBS_ARTIFACT classes matched `https?://\S+` and a bare
filename pattern, so any URL-shaped string discharged them. The same
generator writes the claim and the string that satisfies the check, which
makes presence carry no information about whether the evidence exists.

Measured against the unpatched hook, with the claim and the artifact in the
same block (proximity matters — the checks are unit-scoped):

  https://example.invalid/nope/capture.png        ALLOWED
  `src/totally/made-up.test.ts:42`                ALLOWED
  `evidence/never-captured.png`                   ALLOWED

An artifact now counts only if the author could not have authored its bytes:
a namespace where CI, the upload endpoint or an observability backend writes
them, or a local path that is actually on disk. github.com is author-writable
in general, so only /actions/runs/, /user-attachments/, /blob/, /commit/ and
/pull/ under it qualify.

Controls, before -> after:
  fabricated (want blocked)   1/3 -> 3/3
  genuine    (want allowed)   2/2 -> 2/2

Replaying real published bodies found the interesting case: evidence hosted
in an author-controlled S3 bucket is now refused, correctly — fetching it
unauthenticated proves it is fetchable, not that the author did not write it.
Rather than decide that silently, EVIDENCE_GATE_ARTIFACT_HOSTS registers such
hosts explicitly. Doing so is a visible downgrade from independent to merely
fetchable, which is the point: it should be a choice someone made, not a
property of the regex.

  strict default        -> those bodies block
  bucket registered     -> those bodies pass, fabrications still block

The rule is presence stays necessary and stops being sufficient. Note this is
still not resolution: nothing yet fetches the URL and checks for a 200. An
allowlisted-but-nonexistent /actions/runs/99999999999 link is refused by the
separate CI-restatement rule rather than by this one.
The hook already ran attest-gate.sh in-process on the exact bytes about to be
published, which is a stronger binding than recording a verdict against a
hash — there is no window in which the artifact can be edited after the gate
passes. Two defects meant it often did not run, or ran the wrong gate.

1. _is_evidence_artifact matched `^\*\*Verdict:\*\*`, so where the bold
   stopped decided whether thirteen checks ran:

     **Verdict:** proven     -> all 13 checks
     **Verdict: proven**     -> none

   The same sentence, rendered identically, one of them silently unenforced.
   Now matches a line-leading bolded Verdict however the emphasis falls, still
   anchored so prose mentioning the word does not drag an ordinary reply in.

2. _find_gate ranked $ATTEST_GATE last, behind three default paths. An
   override that loses to a default is not an override: a control run pointing
   it at a stand-in silently exercised the installed gate and reported on that
   instead. This is the failure the skill's own non-negotiable 9 describes —
   an instrument reporting the instruction it was given rather than the effect
   it had — and it was in the resolver for the gate itself.

Verified by substituting a stand-in gate whose exit code is controlled, with
the hook copied to a directory with no sibling scripts/ (as gate-controls.sh
does, for exactly this reason):

                          before   after
  stand-in exits 0        ALLOWED  ALLOWED
  stand-in exits 1        ALLOWED  BLOCKED
  gate path nonexistent   ALLOWED  BLOCKED

gate-controls.sh: all eleven arms behave, including the three negative arms
that catch a gate which has started blocking everything.
…ntmatter mean something

Three controls that were present and inert.

1. evidence-run.yml's determinism check emitted ::warning:: and carried
   continue-on-error, so the step whose own message reads "do not publish
   these numbers" went green and the numbers published. Now ::error:: plus
   exit 1, with continue-on-error removed from that step only. The two runner
   steps keep it, because there the exit code is the verdict and a finding is
   not a failure. Here a difference means the instrument did not return the
   same answer twice, so neither answer is publishable.

   Verified by extracting the step and running it: identical arms exit 0 —
   including the label/log/logs/env fields it deletes by design, which is the
   false positive that taught operators to publish through it — and differing
   arms exit 1.

2. selector-recompute.sh returned 0 for every outcome including
   "VALUE UNSTABLE" and "probe-failed", so a caller gating on the exit code
   saw green on a run whose own artifact says the number is not meaningful.
   The only thing between that and publication was attest-gate happening to
   grep the verdict string out of the prose.

   Now 4 for VALUE UNSTABLE and 5 for probe-failed, with 0 kept for both real
   measurements — a selector that recomputes is a result, not an error. Codes
   documented in the header, and the --help range extended so it shows them.

3. Three of fifty skill.md frontmatters were invalid YAML: `description` was a
   plain scalar containing ": ", which YAML reads as a nested mapping. Any
   tool parsing source frontmatter fails on them; tools/install masked it by
   folding the value to a block scalar on the way out, so the installed copy
   parsed and the source did not.

   Folds evidence and attest to block scalars at rest. The performance skill
   has the same defect but lives on main, so it is fixed in the tools/install
   branch rather than here.

   Round-trip checked: the installed description is byte-identical to the
   source description after folding.

`node --test test/*.test.mjs` 61/61.
`description` was a plain scalar containing ": ", which YAML reads as a nested
mapping, so the frontmatter did not parse. Any tool reading source frontmatter
with a YAML parser fails on it; tools/install masked the defect by folding the
value to a block scalar on the way out, so the installed copy parsed and the
source did not.

Folded to a block scalar at rest. The description text is unchanged.
…aring a schema

The three .graph.json specs name an `executor` and nothing parses them. A run
could skip a node and still describe itself as having followed the graph,
because nothing held the description to the file — which is the failure the
whole package is about, one level up.

What this can and cannot do is worth being exact about, since overstating it
would reproduce that failure. The check, run and expect fields are natural
language; no interpreter evaluates them and this one does not pretend to. A
node's verdict is supplied by whoever ran it.

What is mechanised is the part that was actually being skipped:

  - every node must carry a verdict
  - a pass must name its evidence, because a pass with an empty evidence field
    is a claim about a run, which is refused everywhere else in this package
  - `absent` is a first-class outcome, reported rather than silently dropped
  - a ledger entry naming no node in the graph is an error, not ignored
  - a graph with an unanswered node cannot report complete

Structural validation refuses a graph missing a baseline or a prediction,
those being the shape of every vacuous run: a treatment applied to nothing, or
a result compared to no stated expectation.

  graph-run.mjs <graph.json>                    validate structure
  graph-run.mjs <graph.json> --scaffold         emit a blank ledger
  graph-run.mjs <graph.json> --ledger <l.json>  check a filled-in ledger

Controls: all three graphs validate; blank ledger exit 1; fully answered exit
0; one node silently removed exit 1; pass with empty evidence exit 1; unknown
ledger entry exit 1.
… a claim

Broadening the artifact trigger made it fire on text ABOUT the trigger. A pull
request quoting `**Verdict:** proven` to show what the gate matches was
classified as a validation run and asked for the whole envelope, so
documenting the rule became a violation of it. Fenced blocks are stripped
before the trigger is tested.

  real verdict in prose                    -> artifact
  real verdict, bold-wrapped               -> artifact
  verdict ONLY inside a fence              -> not an artifact
  fenced example alongside a real one      -> artifact
  ordinary reply                           -> not an artifact
  explicit VALIDATION_RUN marker           -> artifact

gate-controls.sh 11/11; chaining controls still verified; fabricated-artifact
controls still 3/3 blocked.

Surfaced by the trigger now working: a real published body is blocked by the
CI-restatement rule, which had never run against it because the old trigger
did not match its verdict line. That rule and this package's own direction
disagree, and the conflict is left for the owner rather than resolved here —
see the pull request description.
The proposal was to cut the prose: every principle with a hook class becomes
one line pointing at the class, leaving only irreducible judgement in text.
That assumes the class exists and fires, and a check NAMED after a principle
is not the same as a check that catches a violation of it.

scripts/principle-coverage.py decides it by measurement. It builds a body
violating one principle and nothing else, publishes it past the gate, and
reports whether the gate blocked it. The clean baseline is a positive control:
if it does not pass, every result is uninterpretable and the script says so —
the first version of this measurement reported 9/9 enforced on a baseline that
was itself blocked.

Result: 3 of 9 enforced, 6 exist only as prose, including items 7, 8 and 9,
which are the three most recently learned. Cutting those would delete the only
place the rule exists, so the cut is not made.

Item 2 is the subtle one and is recorded as such: check 8 keys on a provenance
marker being present anywhere in the body, so a verdict reached by reading
passes as long as an artifact sits nearby. The check is a proxy for the rule.

What the measurement is actually good for is the inverse of the proposal — it
names the six checks worth building, in priority order, and it can be re-run
after each one to show the number move.

`node --test` 61/61; gate-controls 11/11; the output quoted in skill.md is
reproduced by running the script.
The rule is that a validation surface does not restate CI status: "tests are
green at head <sha>" hands the reviewer their own Checks tab back and carries
no information. The implementation matched a bare `actions/runs/N`, which is a
different thing — five of its six branches described a CLAIM about CI, and one
described a URL.

So a link to a specific run and job whose log holds the figure being reported
was refused, and that is precisely what evidence-run.yml exists to produce:
"move the measurement to CI, where the run URL is the capture". The package
forbade its own flagship output, and a real published body was blocked by it.

A run link is now a violation only when it carries restatement language with
it. The distinction is whether the sentence asserts a status the Checks tab
already shows, or points at an execution whose output the Checks tab does not.

scripts/ci-citation-controls.py holds the line, with five restatements that
must be caught and three citations that must be allowed. It imports
CI_RESTATEMENT from the hook rather than restating it, because a control that
tests its own copy of a pattern passes forever while the real one drifts.

  before: 3 of 8 misclassified — every citation a false positive
  after:  0 of 8
  the same controls against the old pattern still catch the citations, so the
  control has power rather than passing by construction

gate-controls 11/11; replaying real published bodies now shows zero
regressions, where the CI rule previously blocked one; principle-coverage
unchanged at 3/9; `node --test` 61/61.
…ying-test' into trial/refresh

# Conflicts:
#	domains/pr-workflow/skills/evidence/hooks/pr-evidence-gate.py
#	domains/pr-workflow/skills/evidence/references/evidence-catalog.md
# Conflicts:
#	tools/install
#	tools/skill-schema.mjs
`tools/check-bundle-contract.sh` parses the array with a single-line `sed`
pattern, so splitting it across lines made the contract check fail to parse it
rather than fail a comparison — the check reported that it could not read the
declaration and exited 2. The comment moves above the declaration.
`hooks/` was added here to `BUNDLE_DIRS` and to the installer's copy list, but
`jongsun/add/pr-validate-falsifying-test` already does both in `edf65a3`, along
with `tools/check-bundle-contract.sh` to assert the two lists agree. Two
branches making the same change is how one of them ends up reverted by a merge
resolution nobody reads.

So this branch keeps only the part that is its own: refusing to overwrite an
installed copy that has local changes. The guard iterates whatever
`BUNDLE_DIRS` holds, so it covers `hooks/` for free once that branch lands.

Dropping it also removes a defect this branch would otherwise carry into main.
`check-bundle-contract.sh` reads the array with a single-line `sed`, and the
multi-line form here made it fail to PARSE rather than fail a comparison — it
exited 2 reporting that it could not read the declaration, which is a check
that has stopped checking rather than one that found something.

Controls re-run after narrowing, since the guard now iterates a different set:
clean install 0, no-op re-install 0, source moves ahead 0 and propagates,
local edit 1 with the sha unchanged, `--force` 0 and discards. 61/61.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant