Skip to content

Releases

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 09 Nov 17:08
v0.11.0
20779c7

Security

  • merge refuses a head it cannot read. (#216)
    The merge read the pull request's head with no check of gh's exit status. A
    gh that 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.
  • verify refuses 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 approved BEHIND pull request up to
    date
    (maintainer decision, 2026-11-09: when Update branch is the only thing
    missing, the agent does it). merge died on BEHIND and the wall denies
    gh pr update-branch, so approved pull requests waited for a human click. The
    command acts only when mergeStateStatus is exactly BEHIND, the author is not
    Dependabot (maintainer deps rebase is that path), this account approved the
    current head and reviewDecision is APPROVED, a valid receipt exists (the
    checks merge applies, 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 with expected_head_sha, never runs
    git on the contributor's tree and never pushes, reads the new head back (bounded
    by UPDATE_READBACK_READS, UNKNOWN counts as not yet) before it reports
    anything, and reports whether the approval survived without approving. POST=off
    rehearses. The deny wall still denies gh pr update-branch, now also
    gh api *update-branch in 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_TREE runs 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.sh or profiles/ exits 2 before any case
    runs. 255 cases that read program source are tagged, and
    MX_SKIP_SOURCE_COUPLED=1 skips 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.sh runs 17 scenarios under env -i with logging stubs and
    writes the argv each stub received, the exit code, stdout, stderr and a hash of
    the state tree. compare.sh exits 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-merge does.
    (#216)
    tests/run-characterization.sh covers the gh pr merge argv, the POST=off
    rehearsal, usage and exit codes by value, the check board, the staleness check,
    identity() failures, stdout against stderr, hostile show arguments and a
    known-answer receipt vector. Each case runs under env -i with stubbed gh,
    podman, docker, curl, wget, ssh and flock.
  • A Rust workspace skeleton.
    (#215)
    rust/ holds mx-core (Fact, Refusal with an exhaustive exit-code table,
    NonEmpty) and mx-ports (traits, fakes and RealProc), a clippy ban on
    Command::new outside mx-ports, scripts/rust-gate.sh and a rust CI job.
    No program is ported yet: the bash programs are the ones that run.

Fixed

  • The sysknife node suite can run setup-contract.test.mjs unmutated. The
    suite ran in the bare node:24-slim image, which has no nc, 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 builds localhost/sk-node:1 from node.Containerfile (node:24-slim
    plus netcat-openbsd), and maintainer-doctor checks the image carries nc
    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.sh alone peaks at 8.7 GB) overlapped, and the kernel hit a
    global OOM twice and killed the terminal. An address-space ulimit does 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 with MemoryMax=12G and 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

Choose a tag to compare

@github-actions github-actions released this 03 Nov 11:48
e7697a5

Fixed

  • An unattended verify extracts the pull request. The timer unit sets
    RestrictSUIDSGID=yes, whose seccomp filter cannot read the struct openat2
    takes and answers ENOSYS. tar 1.35 creates every file through openat2, so
    git archive | tar -x wrote 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 from git read-tree and checkout-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 replaces profiles/, so a refused install had already
    replaced run.sh, defaults.env and every script and stamped VERSION with
    the new release. On 2026-10-29 that left the v0.10.0 deploy reporting
    v0.10.0 after the installer had refused. The check now runs before the
    first write, and a test drives install.sh against a held lock and requires
    zero files written.

maintainer-agent v0.10.0

Choose a tag to compare

@vladimirrott vladimirrott released this 29 Oct 17:39
e1b9e91

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's splitlines(), 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 deps and maintainer screen read the net diff,
    not gh pr diff --patch.
    An mbox carries every commit message, and a
    message is author text: one could hold a forged diff --git and a hunk
    removing evilpkg==0.1, so a pull request adding evilpkg read as an
    upgrade. The classifier refuses mbox input by name, and every test stub for
    gh pr diff fails if --patch is 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-url line
    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-deps now fetches each requirements file at the merge base and the
    head, reads both with scripts/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 new uses: 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. A package.json value
    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). A resolved URL must start with https://registry.npmjs.org/
    exactly, since Node and urlparse disagree about evil.example\\@registry....
    A Cargo.toml literal 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_OK entry 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-deps gives 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 deps uses the same
    bound, and the Dockerfile arm no longer costs quadratic time.

Added

  • verify-deps gates a hash-pinned pip bump. sysknife added a pip ecosystem
    for tools/ci-python on 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 or gh pr merge --admin as the only ways to land it. The
    classifier now has a pip-requirements arm for requirements*.txt whose
    grammar is an allowlist: a name==version pin, an indented
    --hash=sha256: line, a comment. --index-url, --extra-index-url, -e,
    name @ url and 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.py then 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-deps counts declared against verified packages. Each
    refusal test asserts its reason text, and 56 mutants
    (lesson 118).

v0.9.0

Choose a tag to compare

@vladimirrott vladimirrott released this 29 Oct 04:05
834c475

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-deps resolved a pin's comment through commits/<comment>, which follows
    any ref, and GitHub normalises .. in the path first: a pin to a pull request's
    head, commented pull/1/head or v4.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-deps asks git/ref/tags/<tag>, requires
    .ref to be exactly refs/tags/<tag>, follows an annotated tag through at
    most four tag objects to its commit, and asks git/ref/heads/<branch> for
    refs/heads/<branch> before it compares a branch pin.

Added

  • maintainer deps: drive Dependabot pull requests. verify-deps could
    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 the updated-dependencies block 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 one next:: rebase, wait,
    investigate <check>, verify or human, with the changelog link of every
    semver-major member. Any other commit makes it review 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 the next: line is what an unattended run acts on.
  • maintainer deps rebase <pr> posts @dependabot rebase once per head, only
    on a verified Dependabot pull request that is BEHIND or DIRTY and that no
    other actor has leased, reads the comment back, and rehearses at POST=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 reports same_tag per image), deps routes it to verify at patch or
    minor, and verify-deps checks 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, patch or minor),
    DEPS_UNKNOWN_READS (3) and DEPS_UNKNOWN_WAIT (5). The sysknife and magent
    profiles set DEPS_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 every human under ## Ready to merge, awaiting Vladimir with the
    upstream changelog read for each major member.

Changed

  • The deny wall denies gh pr update-branch in 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

Choose a tag to compare

@github-actions github-actions released this 27 Oct 13:58
a959984

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 -->, that scripts/render_figures.py counts
    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. --check refuses 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; --write repairs stale values and is idempotent.
    check_claims.sh and check_drift.py readme-counts count 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-work on disk, and one
    EXIT trap 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 whenever pipefail saw 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
    in bin/, lib/, the installers and the verify suites.

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 16:30
915317d

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 stalled and maintainer-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 hidden fixup-items marker naming the head and a closed list of kinds
    (rebase, test-count, doc-line). stalled lists the pull requests whose
    maintainer review carries one for the current head, with no author commit,
    comment, review or thread reply since, older than FIXUP_QUIET_DAYS (10).
    fixup posts one heads-up, waits FIXUP_NOTICE_DAYS (5), then works in a
    scratch clone with no hook run: it rebases or applies a patch the run wrote
    (capped at FIXUP_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 a Co-authored-by trailer
    for the author, pushes with --force-with-lease against the checked head when
    it rewrites, and reads the head back before it says it worked. Substantive
    items, conflicts, maintainerCanModify=false, bots, action_required runs and
    any author activity are refused in code, and --dry-run and POST=off push
    nothing. The write happens in the script, never through the model. Story 15 is
    no longer a gap. New maintainer 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 under leases/ naming the holder, the note, and when it
    expires; take refuses 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 default LEASE_TTL_SECONDS (3600).

  • A run says it is running. run.sh writes leases/run.json (task, pid, run
    id) right after it takes run.lock and removes it on every exit, through an
    internal maintainer run-lease verb. Liveness is never read from the file or
    a pid: maintainer status asks the lock, so a file left by a killed run
    beside a free lock prints as stale and is ignored. status gains a leases
    block (the run, then each PR lease with holder, note, renewed-ago and
    expires-in; expired ones marked and ignored). An interactive lease take
    while a run is live warns with the run id and maintainer abort, and still
    takes: runs yield to interactive work.

  • gc prunes old leases and the doctor flags forgotten ones. maintainer gc removes a PR lease that expired longer ago than RETENTION_DAYS, never a
    live one, the run lease, or a file it cannot read. maintainer-doctor warns
    about a lease last renewed more than DOCTOR_LEASE_HOURS ago (new default,
    12), naming the PRs and the lease release command; an unreadable lease is a
    warning, never a pass.

  • maintainer-merge and the review prompts honour leases. verify,
    verify-deps and merge ask maintainer lease check first 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-merge does) 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 whose commit_id is 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 through bin/ and lib/, some written twice (container memory in
    two podman run calls, the stale window in the code and in a prompt). Each
    now has one value and one comment, read by bash through
    maintainer_load_defaults and by the python tools through _tunable().
    Precedence is MAINTAINER_<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_DAYS and
    RED_GRACE_HOURS moved in. See docs/configuration.md.

  • maintainer refuses 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-issue and the run snapshot now ask for GH_LIST_LIMIT rows (1000) and
    stop, naming MAINTAINER_GH_LIST_LIMIT, when a listing comes back exactly
    that long. The merged-PR contributor scan stays a window.

  • scripts/check_drift.py holds 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 --help and in docs/cli.md (the command table
    for what a person types, the Called by run.sh section for the rest), every
    key in lib/defaults.env has a row in docs/configuration.md with the same
    default, every MAINTAINER_* name the code reads is documented, every
    docs/*.md path 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 each profile.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 and finish refused anything under three,
    so 12 of 32 magent runs from 2026-10-02 to 2026-10-25 were indexed ABORTED, no report
    with their report in runs/. REPORT_MIN_LINES in lib/defaults.env is now the one
    number, and the suite fails when the preamble disagrees with it. A report that finish
    refuses is now indexed and alerted as report 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 buckets gh reports,
    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 is pass or skipping and 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 described magent as
    running at POST=off while its profile posts; the book called lessons
    "thirty-six defects" and CONTRIBUTING "forty-one". docs/verifying.md and
    docs/release.md, named in five places in the code, did not exist: both are written.
    docs/cli.md described maintainer log as the run's log (it prints the run
    index), omitted run --show-prompt, verify-deps, verify-part and 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 --help lists every verb and works with no profile chosen. The
    docstring listed seven of twenty-two verbs, and --help was not handled: it
    printed to stderr with exit 1, and with two profiles deployed it died in
    profile resolution before it printed anything. bin/maintainer now dispatches
    through one table (COMMANDS), --help, -h and help print 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 run read
    MAINTAINER_PROFILE itself with sysknife as 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 no TASKS runs nothing instead of inheriting four task names.
    install-launchd.sh, install-cron.sh and Install-Maintainer.ps1 require
    the profile name (install.sh --uninstall now strips each deployed profile's
    cron block by name, where it used to strip only the default's). The sysknife
    rev...
Read more

v0.7.2

Choose a tag to compare

@github-actions github-actions released this 21 Oct 21:54
eaeaf74

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-doctor fails when main has 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 on main
    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.
    Under git commit -a, the pre-commit hook carries GIT_INDEX_FILE for 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

Choose a tag to compare

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

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 user prints GitHub's JSON error body on stdout, and the run
    gate, the doctor and maintainer-merge all read that body as the login. The
    alert named '{ "message": "Bad credentials" ... }' as the account. A 401 now
    refuses on the first answer with the gh auth login fix; other failures keep
    the retry-then-75 outage path (lesson 108).
  • verify-deps verifies branch-tracking action pins. A pin commented
    # stable (branch) was refused as "not an action pin", so every Dependabot
    bump touching dtolnay/rust-toolchain needed 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.sh now decodes the PNG and fails on
    anything inside the margin.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 15:59

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 settle turns 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. settle assigns each
    answered offer and reads it back. stale replaces the per-issue timeline read
    that found #440, #428 and #433 nine or more days cold (lesson 106).
  • maintainer offer applies the claim label, and takes --second. The
    label is the reservation other people see while the assignment is impossible.
    --second is 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
    gh comments with no read-back (lesson 106).
  • maintainer file-issue --dry-run. A rehearsal had to be POST=off, a
    profile setting, and a "what does this do" call on sysknife filed #542 titled
    "x" (lesson 105).

Fixed

  • file-issue stops when it cannot read the tracker. _existing_finding
    returned None for a failed read, which is also the answer for "no
    duplicate", so a gh failure filed the finding twice.

  • release unassigns 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.
    holders and offers read 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.
    offer refuses 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. offers and offer share 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.py knew Cargo, npm and GitHub Actions, and answered
    Dockerfile is not dependency metadata for 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
    dockerfile arm refuses a changed line that is not a FROM, a base image
    that stops being pinned by digest, and an added FROM naming an image no
    removed FROM named, which is the no-new-source claim one registry over.
    scripts/resolve_image_digest.py then asks the registry what the tag resolves
    to, over the Registry v2 token dance with no new binary dependency, and
    verify-deps refuses 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, so maintainer offer read as documented
    because maintainer offers is. It now needs a word boundary, which found
    offer and 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 by tayfuryldz, 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: with MAINTAINER_ACCOUNT typed 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, and assign read
    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-deps read 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 reading 0 action pin(s) verified about 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_verify asked whether the mutation changed a file, never which file. A
    suite's suite_mutate_glob lists 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_mutation now reports which
    paths it changed instead of how many, and cmd_verify refuses 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 and pick_suite already 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).

  • screen reports workflow exposure on every verdict, and reads the branch
    rather than the head.
    The warning against approving a queued workflow run
    fired only under DO NOT EXECUTE, and only on .github/workflows/ paths in
    the current diff. sysknife#527 arrived as one commit touching Cargo.lock
    over a branch force-pushed away from 441f163c, 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_pushed event's commit_id is 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 a head_sha on that branch
    that is not among the pull request's commits is a head that existed and no
    longer does. A docs-only change is INERT and 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

Choose a tag to compare

@github-actions github-actions released this 23 Sep 19:00
6dcc32a

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_mutation runs sed only over the files a
    suite's suite_mutate_glob lists, 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 and cmd_verify printed
    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 at CONTRIBUTING.md while the shell suite mutates
    *.sh *.py sysknife-* Makefile *.yml, and read the gate to find out why the
    answer contradicted its own reproduction. apply_mutation now 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.