Skip to content

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 11:33
· 23 commits to main since this release
0115dfe

Changed

  • The skill's version-question flow moved into its own file. Every question about a
    dependency's source loads SKILL.md and INVESTIGATE.md in full, and 30 % of
    INVESTIGATE.md was a block on resolving versions to tags and diffing between them — read
    on every plain source question, used on almost none of them. It is now VERSIONS.md, with a
    route of its own in SKILL.md §5 and a one-line pointer at the end of INVESTIGATE.md, so
    a mis-route costs one extra read rather than a wrong answer.

    Alongside it, a compression pass over what remains: the worker output contract was stated
    twice and is now stated once, the five clickable-link rules became one normalization, and a
    handful of sentences that repeated something said a few lines earlier are gone. Nothing
    behavioural was removed — the capability gate, the trust boundary, the five hard rules and
    the worker prompt's own safety rules are untouched, and the measured partial-clone cost model
    (which git commands fetch blobs, and that git blame fetches one per visited revision) is
    refs' own measurement rather than something the term "partial clone" implies, so it stays
    verbatim.

    A plain source question now loads 20 % less. A version question loads about what it did
    before, plus one extra file read.

Added

  • refs sync and refs doctor now report configuration that has fallen behind its upstream.
    A configured package path is only a locator, and upstream can delete or move what sits at it.
    Until now only refs resolve noticed, for the one package an agent happened to route to, and it
    persisted nothing — so a package deleted upstream could sit wrong in the configuration
    indefinitely while every other package in the same checkout went uninspected.

    Each successful refs sync result now carries a nested structure: {status, packages}, probed
    inside the lock the sync already holds, right after the checkout was updated. Nothing is stored:
    the answer is reported and thrown away, so there is no drift state that can itself go stale. A
    removal and a relocation are reported as different findings, because they need opposite repairs —
    telling an agent to "fix the path" of a package upstream deleted sends it looking for something
    that is not there. Human output gains indented lines under the affected ref and stays silent when
    everything resolves; the summary counts and exit code are untouched, since a drifted ref synced
    perfectly well.

    Only refs that actually sync are probed, which keeps --stale-only a genuine no-op — and is why
    refs doctor gains a config-drift check as the deliberate "check everything now" counterpart.
    It takes each ref's lock with a short timeout and reports the ref as busy rather than waiting,
    writes nothing, and reports warn rather than fail: the configuration has fallen behind,
    nothing is broken. refs list deliberately stays blind — without stored state it would
    turn a cheap inventory command into a locking filesystem sweep.

  • refs resolve answers in one call what used to take three. The skill's investigation flow
    began resolvesyncresolve again, and the third call was not ceremony: package
    verification had described the checkout as it was before the sync, so reusing that answer meant
    reporting a path that no longer necessarily held what it claimed. --sync-if-stale fetches (or
    clones) only when the ref is stale or its checkout absent, and everything it reports describes the
    checkout afterwards. The rule has left the skill and become code.

    It refuses, rather than syncing, when the checkout is unmanaged or unverifiable. sync
    hard-resets and cleans; running it against a directory whose identity was never established is
    how a stray clone loses its history. A failing sync fails the command rather than returning a
    success envelope containing a stale path.

  • refs resolve --project <dir> reports the version a project has installed. The skill used to
    tell the agent to read the project's lockfile by hand, and nothing in refs touched one — so the
    deterministic half of every "what changed between my version and a newer one" question was done
    by the least deterministic component available, against pnpm's peer-qualified keys, aliases,
    overrides and three vendor-specific formats.

    The answer is read from node_modules, walking up in Node's own lookup order, and stops at the
    first installation slot that exists rather than the first readable manifest — falling through to
    an ancestor would report a shadowed install Node would not have loaded. There is deliberately no
    lockfile fallback: a lockfile says what should be installed, node_modules says what is, and
    the second is the question. installed.status is found, not_materialized,
    unsupported_layout (Yarn PnP, detected but never loaded — .pnp.cjs is project code) or
    unverifiable.

  • refs resolve --ref <ref> scopes a query to one ref's packages. A package name registered by
    several refs used to be answered with "use the full ref key" — advice the command could not
    honour, because a full-key query routes by ref and comes back with package: null. The error
    now names a remedy that exists.

  • refs doctor gained a locks check. A held lock used to be invisible: acquisition failed with a
    message that named no owner, and doctor had no lock check at all — so the one command meant to
    answer "is something stuck?" could not see the thing that was stuck. The check lists every entry
    in the locks directory with its recorded owner, how long it has been held, and against which
    window.

    A held lock is not a warning by itself: that is what a concurrent refs sync looks like, so
    it reports ok with the holder listed. warn is reserved for something that will not resolve on
    its own — a recorded process that is gone, a lock past its window and still there, metadata that
    cannot be read, or something that is not a lock at all occupying a lock name. Like every other
    doctor warning, it does not change the exit code.

Changed

  • refs resolve establishes that the path it hands back is really this ref's checkout. It used
    to report presence from a .git entry alone, while add and sync both ran a stronger guard
    before mutating — so the one command whose result is read as "the source is here" was the one
    that did not check what was there. A manual clone at the derived path, a half-finished remove,
    a restored backup or a symlinked second home all produced a confident answer about the wrong
    repository, with no error and no warning.

    Every reply now carries checkout: {status, reason?}managed, missing, unmanaged or
    unverifiable — read straight out of .git/config without spawning git, so the hot path stays
    subprocess-free. The origin URL is never echoed back in reason; it can carry credentials.

    managed requires the core.hooksPath marker to be this home's hooks directory, not merely
    present — the comparison add already makes — so a manual clone that sets it for its own purposes
    does not pass. A config git itself would reject (an unterminated quote, an undefined escape, a
    line that is neither a section nor an assignment) is unverifiable rather than partially read: a
    file git would not accept is not evidence of identity.

    Package verification is gated on it. A manifest read inside an unrelated checkout can answer
    verified for a package that has nothing to do with the query, so anything other than managed
    or missing now yields package.status: "unverifiable" instead of a confident location.

    missing is unchanged and still means checkout.status === "missing". Callers should branch on
    checkout.status, which answers the question missing was often assumed to.

  • The "lock is held" error now says who holds it and for how long. It used to read lock <name> is held — another refs process is running, which left no way to tell a running sync from
    something that crashed 90 seconds ago. It now names the recorded pid, whether that pid is still
    present, how long the lock has been held, and when it becomes reclaimable.

    It deliberately says "recorded pid … is present (identity not verified)" rather than "held by pid
    …": only ESRCH establishes that a process is gone, so a pid that answers may equally be an
    unrelated process that reused the number. And it says "reclaimable", never "released
    automatically" — nothing removes a lock in the background; the phrase means the next acquisition
    attempt is entitled to take it.

  • An operation that ran without the lock it asked for now fails instead of reporting success.
    A lock can still be lost while its holder works — a stolen lock is detected by the next renewal,
    or by release finding a foreign token. Previously the callback's result was returned as if
    nothing had happened. It is now reported as a conflict (exit code 5), because the work ran
    without the mutual exclusion it requested and its result is not trustworthy. An operation that
    failed on its own keeps precedence: its own error is what the caller sees.

Fixed

  • The rm -rf commands refs doctor and refs add suggest are now quoted. Ref keys derive from
    user-supplied urls and permit spaces, $(), backticks, semicolons and quotes, and the refs home
    itself routinely sits under a path containing a space — so pasting an unquoted suggestion could
    delete several wrong paths and leave the intended one, or execute a command substitution embedded
    in a repository name. The form is now rm -rf -- '<path>'; -- additionally stops a path
    beginning with - from parsing as options.

  • A pid in a lock's metadata is now required to be a positive integer within the range
    process.kill accepts. 0 and negative values are
    process-group selectors for process.kill, so metadata carrying either made the liveness probe
    answer for a whole group — reporting a long-gone owner as present, and keeping its lock
    unreclaimable for the rest of its window. A value past that range is worse still: Node rejects it
    with a TypeError rather than an errno, which the probe read as "not gone, therefore present".
    Such metadata is now reported as malformed instead of acted on.

  • A lock is no longer taken away from a holder that is still working, however long the work takes.
    Locks were judged by a fixed ten-minute age: past it, a waiter treated the lock as abandoned and
    stole it even when its owner was demonstrably alive and mid-operation. Since the per-ref lock is held across a whole clone or
    fetch, any repository large enough to take ten minutes could end up with two processes running
    checkout -B / reset --hard / clean -fd against the same directory.

    A holder now renews a lease while it works, so a lock is abandoned when its process is definitely
    gone or its lease has expired — never merely because the work took a long time. "The pid still
    exists" does not override an expired lease, which is what keeps a recycled pid from stranding a
    lock forever.

    The same constant was also too long at the other end: a crashed sync or add left its lock
    behind and blocked the ref for the full ten minutes. An abandoned lock is now reclaimable after
    two minutes rather than ten.

    Two limits are worth knowing. A holder whose event loop is stopped for longer than the lease — a
    suspended process, a sleeping machine — cannot renew, and is stealable after two minutes where it
    used to take ten; in practice a sleeping machine suspends every refs process on it, and the
    pending renewal fires on resume. And a lock written by an older CLI carries no lease and is still
    judged by the ten-minute rule it was written under, so upgrading never dispossesses a running
    older process. The reverse does not hold:
    an older CLI reads no lease, so it can still take a lock from a live current holder once ten
    minutes pass. Holds longer than that during a rolling upgrade are not protected.