Repository navigation
Releases
v0.11.0
Security
mergerefuses a head it cannot read. (#216)
The merge read the pull request's head with no check ofgh's exit status. A
ghthat failed there left the head empty, and the merge ran with
--match-head-commit "", which removes the fence against a push landing
between the last check and the merge. The head must now be read and look like a
commit id before any other fact is read, or the merge refuses (plan decision
F9). Found while writing characterization cases for the gate.verifyrefuses a mutation of production code the pull request did not
change. (#216)
The check asked whether the mutation hit production code and never whether it
hit the diff, so a mutation of any other production file earned a receipt that
spoke for a change nothing there had broken. The refusal names the mutated file
and the production files the pull request changed. An automated review of the
commit that added the production check found it.
Added
maintainer-merge update <pr>brings an approvedBEHINDpull request up to
date (maintainer decision, 2026-11-09: when Update branch is the only thing
missing, the agent does it).mergedied onBEHINDand the wall denies
gh pr update-branch, so approved pull requests waited for a human click. The
command acts only whenmergeStateStatusis exactlyBEHIND, the author is not
Dependabot (maintainer deps rebaseis that path), this account approved the
current head andreviewDecisionisAPPROVED, a valid receipt exists (the
checksmergeapplies, now shared functions), nobody else holds a live lease,
and GitHub lets us push to the head. A fork whose author has not allowed
maintainer edits, or a 403 from the API, exits 3 and says the author must press
Update branch. It calls the REST endpoint withexpected_head_sha, never runs
git on the contributor's tree and never pushes, reads the new head back (bounded
byUPDATE_READBACK_READS,UNKNOWNcounts as not yet) before it reports
anything, and reports whether the approval survived without approving.POST=off
rehearses. The deny wall still deniesgh pr update-branch, now also
gh api *update-branchin any flag order, and allows the wrapper; the three
review prompts name the order verify, approve, update if BEHIND, wait for green,
merge. Lesson 120.MX_TREEruns the offline suite against any tree with the repository
layout. (#213)
The first output line names the tree under test. A tree without
bin/maintainer-merge,lib/run.shorprofiles/exits 2 before any case
runs. 255 cases that read program source are tagged, and
MX_SKIP_SOURCE_COUPLED=1skips them and counts each as a skip, for a tree
whose programs are compiled.- A differential recorder compares two program trees.
(#217)
tests/diff/record.shruns 17 scenarios underenv -iwith logging stubs and
writes the argv each stub received, the exit code, stdout, stderr and a hash of
the state tree.compare.shexits 0 only for zero differences and 2 for any
harness fault. Its blind spots (cross-stub call order, stub environment and
stdin, timestamp format) are recorded in the migration plan before it gates a
port. - 170 characterization cases pin what
maintainer-mergedoes.
(#216)
tests/run-characterization.shcovers thegh pr mergeargv, thePOST=off
rehearsal, usage and exit codes by value, the check board, the staleness check,
identity()failures, stdout against stderr, hostileshowarguments and a
known-answer receipt vector. Each case runs underenv -iwith stubbedgh,
podman,docker,curl,wget,sshandflock. - A Rust workspace skeleton.
(#215)
rust/holdsmx-core(Fact,Refusalwith an exhaustive exit-code table,
NonEmpty) andmx-ports(traits, fakes andRealProc), a clippy ban on
Command::newoutsidemx-ports,scripts/rust-gate.shand arustCI job.
No program is ported yet: the bash programs are the ones that run.
Fixed
- The sysknife node suite can run
setup-contract.test.mjsunmutated. The
suite ran in the barenode:24-slimimage, which has nonc, and that test
probes a Unix socket with netcat. The clean run failed, no receipt was
earnable, and sysknife#749 (test-only, approved) waited two review runs. The
suite now buildslocalhost/sk-node:1fromnode.Containerfile(node:24-slim
plusnetcat-openbsd), andmaintainer-doctorchecks the image carriesnc
and the recipe matches. - Concurrent commits can no longer take the machine out of memory.
(#214,
#216)
On 2026-11-09 four worktrees committed at once. Two shellcheck sweeps
(tests/run-tests.shalone peaks at 8.7 GB) overlapped, and the kernel hit a
global OOM twice and killed the terminal. An address-spaceulimitdoes not
bound shellcheck: it exits 251 on that file at 6 GB and at 12 GB. The sweep now
waits for 10 GiB of free memory and refuses with 75 when it never comes, runs
one shellcheck per script in a systemd scope withMemoryMax=12Gand no swap,
and runs at nice 15. The pre-commit hook takes one lock per machine,
shared by every worktree, in a private directory the user owns: it never
follows a symlink and never truncates the lock file. CI skips the gate and the
cap.
v0.10.1
Fixed
- An unattended
verifyextracts the pull request. The timer unit sets
RestrictSUIDSGID=yes, whose seccomp filter cannot read the structopenat2
takes and answers ENOSYS. tar 1.35 creates every file throughopenat2, so
git archive | tar -xwrote nothing, the pipe dropped tar's status, and the
rust suite failed with a message that blamed the container's network. From
2026-10-29 to 2026-11-02 no unattended run could verify or merge (#158). The
tree now comes fromgit read-treeandcheckout-index, and a count of
tracked files against files on disk refuses a partial or empty tree. Each
guard has its own test, and each was proved by deleting it.maintainer-doctor
has a Sandbox section that extracts a tree inside a transient unit carrying
the directives read from the installed unit. - An install refused for a run in flight writes nothing. The check sat just
above the step that replacesprofiles/, so a refused install had already
replacedrun.sh,defaults.envand every script and stampedVERSIONwith
the new release. On 2026-10-29 that left the v0.10.0 deploy reporting
v0.10.0after the installer had refused. The check now runs before the
first write, and a test drivesinstall.shagainst a held lock and requires
zero files written.
maintainer-agent v0.10.0
verify-deps gates hash-pinned pip bumps, reading each requirements file whole the way pip does, and every dependency arm now checks that a bump names the same things before and after. Several of the holes it closes were in v0.9.0 as shipped; the Security section lists them.
Security
- The dependency classifier reads a diff the way git wrote it. It split the
diff with Python'ssplitlines(), which also breaks at form feed, vertical
tab,\x1c-\x1e, NEL, U+2028, U+2029 and a lone CR, and it skipped any
line starting---or+++as a file header, inside a hunk too. Both hid
content from every arm: a pip line+...\x0c -e git+https://...classified as
a clean bump, and so would a workflow pin line carrying a step after U+2028,
which YAML reads as a line break. The diff is now split on newline alone, any
changed line holding one of those characters is refused, hunk lines are
counted from the@@header instead of guessed from their prefix, the input
must be UTF-8, and a diff that renames, copies or deletes a file, or names a
file without changing a line of it, is refused. 25 recent sysknife Dependabot
pull requests classify exactly as before (lesson 119). verify-deps,maintainer depsandmaintainer screenread the net diff,
notgh pr diff --patch. An mbox carries every commit message, and a
message is author text: one could hold a forgeddiff --gitand a hunk
removingevilpkg==0.1, so a pull request addingevilpkgread as an
upgrade. The classifier refuses mbox input by name, and every test stub for
gh pr difffails if--patchis asked for again. Flagged by an automated
security review of the pushed branch.- A requirements file is read whole, the way pip reads it. A diff shows
three lines of context, and pip's reading depends on lines it does not show:
a trailing backslash joins the next unchanged line (an--index-urlline
swallowed this way read as a clean bump), a# coding:cookie on line 1 or 2
re-decodes the file, and a hash chain can start far above the hunk.
verify-depsnow fetches each requirements file at the merge base and the
head, reads both withscripts/verify_pip_files.py(pip's own line joining,
comment stripping and option split; printable ASCII only, no cookie, no BOM,
no option lines), and refuses unless the pins that moved are exactly the ones
the diff gave. - The other arms check that a bump still names the same thing. A workflow pin
repointed at another owner's action, or a newuses:step, passed: the gate
only asked whether the comment's tag exists in that action's repository. Now
the added and removed pins must name the same actions. Apackage.jsonvalue
must be a semver range under a dependency the manifest already had (a tarball
URL,github:,npm:alias, a dist-tag, a rewritten script and a new"main"
all passed). AresolvedURL must start withhttps://registry.npmjs.org/
exactly, since Node andurlparsedisagree aboutevil.example\\@registry....
ACargo.tomlliteral that moved must be a version, not a git URL or a
package =rename. A Dockerfile stage keeps its name and count. A blank
CARGO_LOCK_OKentry made the Cargo.lock line check pass every line; it is
gone. A line past a hunk's declared counts is refused instead of dropped. verify-depsgives the classifier 60 seconds (MAINTAINER_CLASSIFY_TIMEOUT)
and refuses by name when it runs out, so an input that makes a pattern
backtrack cannot stall an unattended run.maintainer depsuses the same
bound, and the Dockerfile arm no longer costs quadratic time.
Added
verify-depsgates a hash-pinned pip bump. sysknife added a pip ecosystem
fortools/ci-pythonon 2026-10-29, and its first Dependabot bump, #735
(PyYAML 6.0.2 to 6.0.3), was refused as "not dependency metadata", which left
a hand receipt orgh pr merge --adminas the only ways to land it. The
classifier now has apip-requirementsarm forrequirements*.txtwhose
grammar is an allowlist: aname==versionpin, an indented
--hash=sha256:line, a comment.--index-url,--extra-index-url,-e,
name @ urland ranges match none of it and are refused on either side of
the diff, since dropping a private index is a new place to download from as
much as adding one. A bump must move the version of a package the file
already pins, upward, with the same marker and new hashes: a swap, a removal,
a marker flip, a downgrade, a pin moved without its hashes and a package
added twice are each refused. pip reads every space-separated token from the first one
starting with-as an option, quotes or not, so a requirement line with such
a token is refused too: the marker slot cannot carry--config-settings. A hash with no added requirement above it, and a
requirement that arrives without hashes in a hash-pinned file, are refused.
scripts/verify_pypi_release.pythen asks PyPI's JSON API for that release
and refuses a yanked release, one with no files, and any hash that is not one
of its files;verify-depscounts declared against verified packages. Each
refusal test asserts its reason text, and 56 mutants
(lesson 118).
v0.9.0
Dependabot pull requests get their own route through the review task, and verify-deps resolves a pin comment as a tag instead of a path GitHub rewrites.
Security
- A pin comment is a tag GitHub has, not a path GitHub normalises.
verify-depsresolved a pin's comment throughcommits/<comment>, which follows
any ref, and GitHub normalises..in the path first: a pin to a pull request's
head, commentedpull/1/headorv4.2.2/../pull/1/head, read(GitHub agrees)
and earned a dependency receipt (lesson 117, same class as sysknife GHSA-j754).
The classifier now refuses a comment or action path with an empty segment, a
segment starting with.or-, or a character outside a short ref alphabet,
naming the file and line.verify-depsasksgit/ref/tags/<tag>, requires
.refto be exactlyrefs/tags/<tag>, follows an annotated tag through at
most four tag objects to its commit, and asksgit/ref/heads/<branch>for
refs/heads/<branch>before it compares a branch pin.
Added
maintainer deps: drive Dependabot pull requests.verify-depscould
receipt a pure dependency bump and nothing drove it: the review treated a
Dependabot pull request like a contributor's, and sysknife #688-#692 sat a
day untouched.deps [pr...]lists every open pull request by
app/dependabot, or the ones named, and prints one block each: the identity
it checked (the branch in this repository, every commit authored by
dependabot[bot], committed by it or by GitHub, and signed: GitHub derives a
commit's login from its email, so only the signature is out of a pusher's reach),
the members of theupdated-dependenciesblock with their semver class, the
classifier's verdict, the CI counts (a check whose conclusion is the empty
string is pending) and the merge state, then onenext::rebase,wait,
investigate <check>,verifyorhuman, with the changelog link of every
semver-major member. Any other commit makes itreview as a contributor PR.
It exits non-zero naming every pull request it could not read, and every
name it prints that GitHub returned, check names included, is flattened to
one line and capped, since thenext:line is what an unattended run acts on.maintainer deps rebase <pr>posts@dependabot rebaseonce per head, only
on a verified Dependabot pull request that isBEHINDorDIRTYand that no
other actor has leased, reads the comment back, and rehearses atPOST=off.- A Docker digest bump can be verified. Dependabot writes no
update-type
when a base image keeps its tag and only the digest moves. When every changed
file is a Dockerfile and every image keeps its image and tag (the classifier
now reportssame_tagper image),depsroutes it toverifyatpatchor
minor, andverify-depschecks the digest against the registry. A tag change
or any other change without a semver class still goes to a person. - New defaults
DEPS_AUTOMERGE(minor;none,patchorminor),
DEPS_UNKNOWN_READS(3) andDEPS_UNKNOWN_WAIT(5). The sysknife and magent
profiles setDEPS_AUTOMERGE="minor". - The sysknife, magent and template review prompts drive Dependabot pull requests with
maintainer deps: no reply to the bot, no next-issue invite, one merge at a
time, and everyhumanunder## Ready to merge, awaiting Vladimirwith the
upstream changelog read for each major member.
Changed
- The deny wall denies
gh pr update-branchin every profile. A commit of
ours on a Dependabot branch stops Dependabot rebasing it for good, and on a
contributor's branch it is a push the doctrine already forbids.
v0.8.1
A verify cleans up after itself on every exit and keeps its scratch on disk, no
shipped script pipes into grep -q, and every figure in the docs is rendered
from its source.
Changed
- Every figure in the README and the docs is rendered from its source, never
typed. A number that describes the project is a marker,
<!-- fig:NAME -->VALUE<!-- /fig -->, thatscripts/render_figures.pycounts
from the tree. The suite size and the lesson count are a rounded floor ("1,300+",
"110+"), so a pull request that adds a test or a lesson no longer hand-edits three
to five lines; backends, evals, denied verbs, profiles, user stories and gaps stay
exact and are rendered too.--checkrefuses a stale marker, an unknown name, a
malformed or nested marker, a marker in a code fence, a figure typed outside a
marker, a site that stopped stating its figure, a tree with no markers and a
source it could not read;--writerepairs stale values and is idempotent.
check_claims.shandcheck_drift.py readme-countscount through the same module
and no longer match typed numbers, the hook and CI run the figure check, and the
drift cases that mutated literal counts mutate marker contents. Two drift tests
had gone vacuous because they matched the literal 110.
Fixed
- A verify no longer leaves the pull request behind (#111). The extracted
tree and its cargo target now live under$STATE/verify-workon disk, and one
EXITtrap removes them on every exit, a refusal included. On a tmpfs/tmp
two refused verifies had kept 12 GB in RAM and pushed the next verify's rustc
past its container's memory limit (lesson 115). - No shipped script pipes into
grep -q(lesson 116). Four sites read a
match as a failure wheneverpipefailsaw the writer take SIGPIPE: fixup's
sign-off check (seen flaking), prune's open-PR keep test, the doctor's timer
check and fixup's test-count check. The suite now refuses the shape anywhere
inbin/,lib/, the installers and the verify suites.
v0.8.0
A timer run and an interactive session can see each other through leases, a pull
request stalled on mechanical review items can be finished by a script, and
every tunable has one default that the docs are checked against.
Added
-
maintainer stalledandmaintainer-repo fixup: finish a pull request the
author left on mechanical review items. Contributor pull requests sat on
CHANGES_REQUESTED for days when all that remained was a rebase, a test count
that moved under them or one doc line, and the doctrine forbade touching their
branch (story 15). The review prompts now end every CHANGES_REQUESTED body with
a hiddenfixup-itemsmarker naming the head and a closed list of kinds
(rebase,test-count,doc-line).stalledlists the pull requests whose
maintainer review carries one for the current head, with no author commit,
comment, review or thread reply since, older thanFIXUP_QUIET_DAYS(10).
fixupposts one heads-up, waitsFIXUP_NOTICE_DAYS(5), then works in a
scratch clone with no hook run: it rebases or applies a patch the run wrote
(capped atFIXUP_MAX_LINES, 20; never production,.github/or anything the
screen calls executable; a test count must match the profile's own
FIXUP_COUNT_CMD), commits as the maintainer with aCo-authored-bytrailer
for the author, pushes with--force-with-leaseagainst the checked head when
it rewrites, and reads the head back before it says it worked. Substantive
items, conflicts,maintainerCanModify=false, bots,action_requiredruns and
any author activity are refused in code, and--dry-runandPOST=offpush
nothing. The write happens in the script, never through the model. Story 15 is
no longer a gap. Newmaintainer fixup-check <pr>prints the check the script
runs. -
maintainer lease: say who is working a pull request. A timer run and an
interactive session worked #637, #658 and #660 at the same time on 2026-10-25
(docs/lessons.md 8 and 80 are the same fault), and the run's report could not
tell whose review it had found.lease take <pr>... [--ttl D] [--for TEXT]
writes a small file underleases/naming the holder, the note, and when it
expires;takerefuses another holder's live lease and names them, renews
its own, and prints a takeover of an expired one;check <pr>exits 0 free or
yours, 1 held, 2 unreadable (never free);release <pr>...frees yours. The
read-decide-write step runs under a short flock, so of two simultaneous takes
exactly one wins. New defaultLEASE_TTL_SECONDS(3600). -
A run says it is running.
run.shwritesleases/run.json(task, pid, run
id) right after it takesrun.lockand removes it on every exit, through an
internalmaintainer run-leaseverb. Liveness is never read from the file or
a pid:maintainer statusasks the lock, so a file left by a killed run
beside a free lock prints as stale and is ignored.statusgains aleases
block (the run, then each PR lease with holder, note, renewed-ago and
expires-in; expired ones marked and ignored). An interactivelease take
while a run is live warns with the run id andmaintainer abort, and still
takes: runs yield to interactive work. -
gcprunes old leases and the doctor flags forgotten ones.maintainer gcremoves a PR lease that expired longer ago thanRETENTION_DAYS, never a
live one, the run lease, or a file it cannot read.maintainer-doctorwarns
about a lease last renewed more thanDOCTOR_LEASE_HOURSago (new default,
12), naming the PRs and thelease releasecommand; an unreadable lease is a
warning, never a pass. -
maintainer-mergeand the review prompts honour leases.verify,
verify-depsandmergeaskmaintainer lease checkfirst and refuse a PR
another actor holds, naming the holder; a lease they cannot read, or no helper
to ask, refuses too. The preamble and the review prompts tell a run to check
the lease before posting and to skip a leased PR in its report. The lease
verbs need only a state directory, so when the caller names one (as
maintainer-mergedoes) they answer with several profiles deployed and none
chosen; without one they still refuse to guess a project. -
maintainer reviewed <pr>. Lists the reviews and inline comments by the
profile's account whosecommit_idis the PR's head now, plus the lease. A
pure read, derived from GitHub on every call; a read that fails refuses
instead of reporting none. The review prompts use it instead of a hand-built
activity loop. docs/maintainer-doctrine.md gains "Who yields to whom". -
lib/defaults.env: one home for every tunable. Defaults were literals
scattered throughbin/andlib/, some written twice (container memory in
twopodman runcalls, the stale window in the code and in a prompt). Each
now has one value and one comment, read by bash through
maintainer_load_defaultsand by the python tools through_tunable().
Precedence isMAINTAINER_<KEY>in the environment, then a suite's own
declaration (suite_memory,suite_cpus,suite_pids,suite_tmpfs), then
profile.env, then the default. New keys:VERIFY_MEMORY,VERIFY_CPUS,
VERIFY_PIDS,VERIFY_TMPFS,VERIFY_TIMEOUT,PREFETCH_TIMEOUT,
MCP_VERIFY_SLACK,STALE_HOLD_DAYS,GH_LIST_LIMIT,CONTRIBUTOR_WINDOW,
GH_TRIES,GH_BACKOFF,DOCTOR_RUN_LIMIT,DENY_WALL_MIN_RULES,
OPENCODE_MODEL;LOCK_WAIT,RETENTION_DAYS,CLAIM_STALE_DAYSand
RED_GRACE_HOURSmoved in. See docs/configuration.md. -
maintainerrefuses a listing that fills its limit. gh cuts a list
silently at--limit, so a roster past the limit lost its tail and read as
"nobody else is available" or "no duplicate".claims,offers,holders,
file-issueand the run snapshot now ask forGH_LIST_LIMITrows (1000) and
stop, namingMAINTAINER_GH_LIST_LIMIT, when a listing comes back exactly
that long. The merged-PR contributor scan stays a window. -
scripts/check_drift.pyholds the documentation to the code. Seven
checks, each proved by planting its defect in a copy of the tree: every verb
the code dispatches is in--helpand indocs/cli.md(the command table
for what a person types, theCalled by run.shsection for the rest), every
key inlib/defaults.envhas a row indocs/configuration.mdwith the same
default, everyMAINTAINER_*name the code reads is documented, every
docs/*.mdpath a message names exists, the README's backend count and the
lesson counts in the README, CONTRIBUTING and the book agree with the tree,
the README's Posts column matches eachprofile.env, and every CHANGELOG
version has a link at the foot. The pre-commit hook, CI and the test suite
run it.
Fixed
-
A quiet run's report is no longer refused for doing what the preamble asked (#133).
The preamble asked for a two-line report andfinishrefused anything under three,
so 12 of 32 magent runs from 2026-10-02 to 2026-10-25 were indexedABORTED, no report
with their report inruns/.REPORT_MIN_LINESinlib/defaults.envis now the one
number, and the suite fails when the preamble disagrees with it. A report thatfinish
refuses is now indexed and alerted asreport refused by finish, with the reason, not
as a missing report. Lessons 111 to 113. -
A cancelled check no longer passes the merge gate.
maintainer-merge merge
counted failing and pending checks and ignored the other bucketsghreports,
and never compared what it accepted with the total, so a cancelled check, a
bucket it did not recognise and an empty check list ([]) all read as a green
board. It now refuses unless every check ispassorskippingand at least
one passed, and names the count (2 of 3 checks are pass or skipping). No
test had ever fed it a pending or cancelled bucket; there are nine cases now,
each proved by removing its refusal. -
The documentation matches the code again. The README said three backends
(four exist), "twenty-five" lessons beside 110, and describedmagentas
running atPOST=offwhile its profile posts; the book called lessons
"thirty-six defects" and CONTRIBUTING "forty-one".docs/verifying.mdand
docs/release.md, named in five places in the code, did not exist: both are written.
docs/cli.mddescribedmaintainer logas the run's log (it prints the run
index), omittedrun --show-prompt,verify-deps,verify-partand the
[suite]argument, and the CHANGELOG's links stopped at 0.4.1. Twenty-odd
environment variables the code reads were documented nowhere; they are in
docs/configuration.md. -
maintainer --helplists every verb and works with no profile chosen. The
docstring listed seven of twenty-two verbs, and--helpwas not handled: it
printed to stderr with exit 1, and with two profiles deployed it died in
profile resolution before it printed anything.bin/maintainernow dispatches
through one table (COMMANDS),--help,-handhelpprint it to stdout
and exit 0, and a wrong argument count prints that verb's usage. The test
suite reads the verb list from the same table.maintainer-merge's header now
lists its six verbs, not three.
Changed
- No tool guesses which project it acts on.
maintainer runread
MAINTAINER_PROFILEitself withsysknifeas the fallback, so with one other
profile deployed it started the wrong repository's run; it now uses the
profile resolved once at import, and a machine with none refuses. A profile
that declares noTASKSruns nothing instead of inheriting four task names.
install-launchd.sh,install-cron.shandInstall-Maintainer.ps1require
the profile name (install.sh --uninstallnow strips each deployed profile's
cron block by name, where it used to strip only the default's). The sysknife
rev...
v0.7.2
The doctor now watches the repository it maintains, and the test suite stops
writing into the index of the commit that runs it.
Added
maintainer-doctorfails whenmainhas been red for hours. sysknife's
main sat red for three days on four rustls advisories and nothing in the
agent noticed, while every open pull request showed the failing required
check. The doctor now reads the latest push run of each workflow onmain
and fails on one red for six hours or more; a run list it cannot read is a
warning, not a pass (lesson 109).
Fixed
- The test suite no longer writes into the index of the commit running it.
Undergit commit -a, the pre-commit hook carriesGIT_INDEX_FILEfor the
commit in progress, and the suite's scratch repositories added their fixture
files to it. The shellcheck sweep then saw no scripts and refused the commit.
The suite now unsets git's per-repository variables before any case runs
(lesson 110).
v0.7.1
Two fixes to gates that refused correctly and then told the human the wrong
thing, or nothing they could act on.
Fixed
- A rejected GitHub token is reported as rejected, not as a wrong account.
On a 401,gh api userprints GitHub's JSON error body on stdout, and the run
gate, the doctor andmaintainer-mergeall read that body as the login. The
alert named'{ "message": "Bad credentials" ... }'as the account. A 401 now
refuses on the first answer with thegh auth loginfix; other failures keep
the retry-then-75 outage path (lesson 108). verify-depsverifies branch-tracking action pins. A pin commented
# stable (branch)was refused as "not an action pin", so every Dependabot
bump touchingdtolnay/rust-toolchainneeded a hand-written receipt
(sysknife#599). The gate now asks GitHub's compare whether the SHA is on that
branch: identical or behind passes, anything else refuses
(lesson 107).
Changed
- The README mark is centred, and a test keeps the social preview inside its
130 px safe zone. The old preview put content 95 px from an edge while the
asset notes claimed 130 px;run-tests.shnow decodes the PNG and fails on
anything inside the margin.
v0.7.0
The tool learning to hand work out the way the tracker actually allows. An
offer on a fresh issue cannot be an assignment, so offers now reserve the issue
with a label, settle assigns once the holder speaks, and stale finds the
holds nobody touched. Moves the middle digit: file-issue now refuses when
it cannot read the tracker, and release now unassigns, so two calls that
used to succeed one way now behave another.
Added
maintainer settleturns answered offers into assignments, and
maintainer stale [days]names cold holds. GitHub drops an assignee who is
not a collaborator and has not posted on the issue, so the 22 issues filed on
sysknife on 2026-10-01 could be offered and not assigned.settleassigns each
answered offer and reads it back.stalereplaces the per-issue timeline read
that found #440, #428 and #433 nine or more days cold (lesson 106).maintainer offerapplies the claim label, and takes--second. The
label is the reservation other people see while the assignment is impossible.
--secondis the maintainer overriding the one-offer rule per call; on
2026-10-01 the rule refused a requested batch and the offers went out as bare
ghcomments with no read-back (lesson 106).maintainer file-issue --dry-run. A rehearsal had to bePOST=off, a
profile setting, and a "what does this do" call on sysknife filed #542 titled
"x" (lesson 105).
Fixed
-
file-issuestops when it cannot read the tracker._existing_finding
returnedNonefor a failed read, which is also the answer for "no
duplicate", so aghfailure filed the finding twice. -
releaseunassigns the person and removes the claim label. The comment
alone left #440 assigned and labelled until both were removed by hand. -
maintainer offer <issue> <user> <text>posts an offer on the issue's own
thread, and only when nobody holds it.holdersandoffersread an
issue's thread, and until now an offer was whatever prose the maintainer typed
wherever they were typing it. On 2026-10-01 three sysknife offers lived only in
pull-request reviews (#530 on #528, #474 on #514, #411 on #515), and two were
offered again within hours: #474 to a second contributor while the first held
it, and #411 five hours after its holder had accepted it on #411 itself.
offerrefuses an issue somebody holds, one an open pull request closes, one
that is reserved, assigned or claimed, anyone who already holds an open offer,
and text naming a second person. It refuses when the open pull requests cannot
be read, and reads the comment back.offersandoffershare one
_offer_ledger, so the refusal and the printout cannot disagree. Seven tests,
two of them pinned to their reason because they passed while the command did
not exist yet (lesson 104). -
A dependency bump may now be a base-image digest bump.
classify_dependency_bump.pyknew Cargo, npm and GitHub Actions, and answered
Dockerfile is not dependency metadatafor anything else. sysknife pins its
build images by manifest-list digest so a moved tag cannot change the build,
and Dependabot bumps them, so every such pull request had only two ways to
land: an asserted receipt, which an unattended run may not write, or
gh pr merge --admin, which is the path this gate exists to remove. The new
dockerfilearm refuses a changed line that is not aFROM, a base image
that stops being pinned by digest, and an addedFROMnaming an image no
removedFROMnamed, which is the no-new-source claim one registry over.
scripts/resolve_image_digest.pythen asks the registry what the tag resolves
to, over the Registry v2 token dance with no new binary dependency, and
verify-depsrefuses a digest that is not the answer. Nine tests. The four
refusals assert their own reason, because while the classifier still refused
every Dockerfile for being a Dockerfile, a bare"verdict": "refused"passed
for a code path the test was not about (lesson 101). -
The docs-coverage check matches whole command names. It searched the docs
for each command as a substring, somaintainer offerread as documented
becausemaintainer offersis. It now needs a word boundary, which found
offerand nothing else. -
A GitHub login is compared the way GitHub compares it: without case.
sysknife#519 was offered to@TayfurYldz, the pull request that closed it
came back from the API authored bytayfuryldz, and the gate refused to
merge the offered person's own work because it compared the two strings
exactly. They are one account, id 238304586. The same comparison ran about
twenty times across the offer tracker, the gate and the identity checks, and
one of them failed open: withMAINTAINER_ACCOUNTtyped in any case other
than the API's, every maintainer comment read as somebody else's,holders
found nobody, and the gate let a merge through over a live offer. A release
spelled in another case from its offer retracted nothing, andassignread
its own successful assignment back as GitHub having dropped it. Mentions are
canonicalised where they are parsed, every login read from the API goes
through one_login(), and the five identity guards compare
case-insensitively, which refuses exactly the accounts they refused before
because no two GitHub accounts differ only in case. The refusal also printed
its holder list with nothing after the last name, so it read
TayfurYldzwas pointed at. Seven tests, each mutation-proved; two of them
exist because the first five could not tell a lowercased source from a
case-insensitive match (lesson 103). -
A verifier that inspected nothing no longer looks like one with nothing to
inspect.verify-depsread its action pins with
while ... done < <(python3 -c ...), and process substitution reports the
status of the redirection rather than of the command inside it. A producer
that died left the loop reading an empty stream, the counter at zero, and a
sealed receipt reading0 action pin(s) verifiedabout a diff that bumped a
pin. Both loops now read from a variable, so a dead producer refuses, and each
is followed by an equality against the count the classifier declared: zero
checked out of one declared is a check that did not run, not a clean bump.
Mutation-proved in both directions (lesson 102).
Two guards, both from the same session's work on sysknife, and both about a
check that answered a narrower question than the one being asked.
-
A mutation that lands only outside production code earns no receipt.
cmd_verifyasked whether the mutation changed a file, never which file. A
suite'ssuite_mutate_globlists file types, so sysknife's rust suite mutates
*.rs, which is every test file as well as every source file. A mutation
aimed at the contributor's own test passed every check and minted a receipt,
having proved that a test notices its own edit. Observed twice on 2026-09-29
in the reviewer's own work: an MCP boundary test that called the sanitiser
directly rather than driving the route stayed green with the route's wiring
deleted, and a full-view renderer test used only short lines so a reverted
length cap changed nothing it asserted.apply_mutationnow reports which
paths it changed instead of how many, andcmd_verifyrefuses when the diff
carries production code and the mutation reached none of it, printing what it
did change. Only asked when the diff has production code at all, because a
docs-only pull request has nothing to mutate andpick_suitealready decides
what its receipt may claim. Two tests: one mutation into a test file, refused,
and one into the production file beside it, which still earns a receipt,
because a guard that refuses everything is not a guard
(lesson 99). -
screenreports workflow exposure on every verdict, and reads the branch
rather than the head. The warning against approving a queued workflow run
fired only underDO NOT EXECUTE, and only on.github/workflows/paths in
the current diff. sysknife#527 arrived as one commit touchingCargo.lock
over a branch force-pushed away from441f163c, which had edited
.github/workflows/ci.yml; its runs were still in the approval queue and the
author meant to restore it. The timeline cannot answer this, because a
head_ref_force_pushedevent'scommit_idis the commit the ref points at
after the push and never names what was dropped. The workflow runs can: every
push queues runs pinned to the head it pushed, so ahead_shaon that branch
that is not among the pull request's commits is a head that existed and no
longer does. A docs-only change isINERTand its queued run is exactly as
dangerous as anyone else's, so the judgement now prints on every verdict, and
an unreadable history says so and says to treat the approval as unsafe rather
than going quiet. Five tests, one of them a force-push over an innocent commit
so the guard cannot pass by crying wolf (lesson 100).
v0.6.2
The gate telling a contributor their guard does not bite, when the mutation had
not touched a single file. Found by the agent, in a run of the release it was
shipped in, reading its own tool after the answer contradicted its evidence.
Fixed
-
A mutation that changes nothing is refused as a no-op, not reported as a
guard that does not bite.apply_mutationruns sed only over the files a
suite'ssuite_mutate_globlists, so a mutation aimed at a file outside that
list, or an expression that matches no line, left the tree untouched. The
mutated run then passed for the gate's own reason andcmd_verifyprinted
THE GUARD DOES NOT BITE, which is a verdict about the contributor's test
from a mutation that never happened. sysknife's review run hit it on
2026-09-23 with a sed aimed atCONTRIBUTING.mdwhile the shell suite mutates
*.sh *.py sysknife-* Makefile *.yml, and read the gate to find out why the
answer contradicted its own reproduction.apply_mutationnow counts the
files it changed and the caller refuses at zero, naming the glob.xargs -r
also stops an empty file list leaving sed reading stdin.The case that was supposed to cover this asserted the old message, so it
passed while the gate conflated the two findings. It now requires the accurate
one and fails on the other, and a second case reproduces the real shape: a
file that exists, a sed that matches it, and a suite that does not mutate it.