Search guides, workflows, and reference pages.

Docs/tools

skill-lock

Capability: substrate:action-guard

Harness: agnostic

A per-skill write lock for the batch skills. A sweep’s read and classify phase has no side effects and may run concurrently with anything. Its write phase (posting triage comments, stale nudges, labels, closes) must not: two overlapping runs of the same skill over the same repository post the same comment or label twice, and a doubled bot comment is exactly what makes contributors distrust the bot. The write phase takes this lock first; a second run finds it held, stops, and says who holds it.

Prerequisites

  • Runtime: Python 3.11+ via uv; stdlib only.
  • CLIs: none.
  • Credentials / auth: none.
  • Network: none; works entirely on local files.

Where the lock lives

<personal layer>/run/locks/<skill>@<owner>__<repo>.lock/, where the personal layer is .apache-magpie-local/ in an adopted project and the git-directory home (<git-common-dir>/apache-magpie/) otherwise, the same resolution every framework tool uses (layers.py). It is never inside the working tree of an adopted project, so it can never be committed.

The lock is scoped to skill and repository: issue-triage on apache/a and on apache/b never contend, and issue-triage and issue-stale-sweep on the same repository never contend either, because they post different things.

Liveness

The process that runs acquire is an uv run that exits at once, and the skill run that owns the lock is an agent session with no stable pid, so liveness is a lease: the owner record carries an expiry (--ttl, default 7200 s) and the write phase is expected to finish well inside it. An expired lock is stale and is taken over by the next acquire; renew extends a long run; pass the --token from acquire so a run whose lease was taken over cannot extend the new holder’s lock. A takeover renames the stale lock to a unique tombstone first, so when two runs find the same stale lock only one of them acquires it. A caller that does have a stable pid (a shell script) passes --owner-pid, and a dead pid on the same host also counts as stale.

CLI

Every command prints one JSON object.

uvx --from ~/.claude/magpie/skill-lock skill-lock acquire <skill> --repo <owner/repo> [--ttl 7200] [--note "..."]
uvx --from ~/.claude/magpie/skill-lock skill-lock renew   <skill> --repo <owner/repo> [--ttl 7200] [--token <t>]
uvx --from ~/.claude/magpie/skill-lock skill-lock release <skill> --repo <owner/repo> [--token <t>] [--force]
uvx --from ~/.claude/magpie/skill-lock skill-lock status  <skill> --repo <owner/repo>
uvx --from ~/.claude/magpie/skill-lock skill-lock list

Exit codes for acquire:

Exit status Meaning
0 acquired Proceed. The result carries a token; pass it to release.
3 already_running Another run holds the lock. Stop the write phase. The result carries the holder record (started, expires, note) and a hint with the --force release command for a run known to be dead. Not a failure of the sweep.
2 error Could not create or read the lock; the result says why.

release with --token refuses a non-matching holder (so a run never releases a lock it did not take); without a token it releases unconditionally, which is what the skill prose does at the end of its own run. --force is the operator’s override for a lock whose owner is known to be gone before its lease expired.

--root names the project root explicitly; by default it is found from the current directory (the nearest ancestor holding .git, .svn or .apache-magpie.lock).

Where it is installed

Skills run it as uvx --from ~/.claude/magpie/skill-lock skill-lock … on every install. It ships inside the always-installed magpie-vetted-ops plugin, whose session hook points ~/.claude/magpie/skill-lock at the installed version, the same way it links ~/.claude/magpie/vetted-ops. The plugin cache is read-only, which is why the launcher is uvx --from (it builds into uv’s cache) rather than uv run --project (which would need to write a venv into the plugin). It runs inside the sandbox and needs no exclusion: it touches only the lock directory.

How skills use it

The shared prose block tools/dev/blocks/write-lock.md is propagated into the write step of every batch skill that posts: issue-triage, security-issue-triage, issue-stale-sweep, pr-stale-sweep, pr-management-triage and good-first-issue-sweep. Read-only skills take no lock. The block says: acquire before the first write, stop on already_running and show the holder, release after the recap, and release in the stop-and-surface path too so a failed post never leaves a lock behind for its lease length.

Tests

uv run --directory tools/skill-lock --group dev pytest
Suggest a change