Scope: All project roots listed in your lock_roots.json.
Canonical spec: This file. Script-level docs are in the individual .py files.
Updated: 2026-06-25
Central coordination principle for parallel work by multiple agents, automated
loops, or humans: a LOCK*.txt file in a project directory signals that the
project or a component is in use -- no agent or automated loop modifies that
area while a valid, non-expired lock exists.
Fastest ways to see active locks (in order of speed):
- Search tool (fastest, live): search for
LOCK*.txtfiles in the relevant root using your file-search tooling. - Cache file (no scan needed): read the auto-generated
LOCK-CACHE.md(written bylock_scan.py --write-cache). - Script:
python lock_scan.py(read-only list) orpython lock_scan.py --write-cache(refresh cache).
The LOCK*.txt files themselves are always authoritative; the cache is a
derived quick-index only.
watcher/ provides an optional local daemon, REST API, and browser UI. It uses
the same repository-root scripts and config:
lock_roots.jsonlock_scan.pylock_utils.pyprune_stale_locks.py
The watcher does not change the protocol. LOCK*.txt files remain the
authoritative source of truth; SQLite, generated caches, REST responses, and
the UI are derived views.
Runtime data is stored outside the repository by default:
~/.lock_master_watcher/watcher.db
~/.lock_master_watcher/daemon_status.json
Override with LOCK_MASTER_WATCHER_DATA=/path/to/runtime.
Start from the repository root:
python watcher/lock_watcher.py --update-cache
python watcher/web_server.py --port 8095Windows shortcut:
watcher\START.batOpen http://127.0.0.1:8095. The web server is intended for local use only.
Watcher scan model:
- full scan every 60 seconds
- quick check of known active locks every 20 seconds
- daemon heartbeat every 5 seconds
- directory statistics every 15 minutes
- same-host singleton detection through PID and heartbeat
LOCK.txt-- entire project locked (scope =project).LOCK.<scope>.txt-- only that component locked; free scope name (sub-area / sub-folder), e.g.LOCK.frontend.txt,LOCK.api.txt,LOCK.mobile.txt.LOCK.team.<host>.txt-- Team Lock for the whole project (see below).LOCK.team.<scope>.<host>.txt-- Team Lock for a specific component (see below).LOCK.user.txt/LOCK.user.<scope>.txt-- User Lock (see below): user-owned full lock, removed ONLY by the user.- Multiple agents can work in parallel on different components of the same project using different scoped locks.
- Detection regex:
^LOCK(\.[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*)?\.txt$ - Legacy
TEST.txt/TESTS.txt-- deprecated, do not create new ones (still recognised as a lock, but not subject to automatic expiry).
Template: LOCK_TEMPLATE.txt. Lines starting with # = comment; blank lines
are ignored.
| Field | Required | Meaning |
|---|---|---|
owner |
yes | Who holds the lock (agent / user / automation). |
created |
yes | ISO timestamp YYYY-MM-DDTHH:MM (base for expiry). |
host |
optional | Machine/hostname holding the lock — which system locked it (cross-system). |
expires_after |
optional | e.g. 24h / 48h / 90m. Default = 24h. |
release_condition |
optional | Free text: what must happen for the lock to be released. |
mode |
optional | hard (no changes, default) | soft (reads/hints ok). |
purpose |
optional | Free text: why locked / what is running. |
scope |
optional | Informational only; the filename is authoritative. |
If created is missing or unparseable, the file's mtime is used as fallback
for expiry calculation.
LOCK.txt or LOCK.<scope>.txt -- locks the area for all systems and all
agents. No other system or agent may modify the locked area while the lock
is active. Use this when only one system should work in an area at a time.
LOCK.team.<host>.txt or LOCK.team.<scope>.<host>.txt -- coordinates
multiple agents within one system (e.g. several parallel agents on the
same machine). It signals "my system is active here; other systems should
stay out."
Why per-system, not cross-system? Cloud-sync latency (30 s -- 5 min with OneDrive, Dropbox, or other shared filesystems) makes real-time coordination across system boundaries unreliable. Each system manages its own agents internally via the Team Lock; cross-system exclusion is achieved by the presence of the Team Lock file itself (other systems see it and stay out).
When a second system wants to enter the same scope:
- If the scopes do not overlap: it may create its own
LOCK.team.<scope>.<its-host>.txtfor its slice. - If the scopes overlap: treat the existing Team Lock like an Exclusive Lock -- wait or choose a different task.
Conflict copies (cloud-sync rename collision): When two systems write a Team Lock simultaneously, one rename wins and one becomes a conflict copy. The system whose file survived continues; the other must back off and retry. On NTFS and most cloud-sync filesystems, a rename within the same directory is atomic and can be used as a lightweight claim mechanism.
A Team Lock must contain all four sections (use TEAM_LOCK_TEMPLATE.txt):
- Presence log -- loop ID, agent name, role, main task, start time. Every agent checks in here before working; removes its entry when done.
- File/folder claims + queue -- who is editing what; who is waiting.
- Tool/software/MCP claims + queue -- exclusive resources (e.g. a running server, a DB connection, a specific MCP tool). Only claim what is truly exclusive; keep claims tight.
- Messages, tips, lessons learned -- short handovers, warnings, notes for other agents on the same team.
- Check in before working: add your presence entry before touching files.
- Rotate roles when requested by the team coordinator (first-in agent or designated lead).
- Choose a complementary slice if a resource is already claimed; do not double-claim.
- Update claims on task change: if you switch to a different area, update your claim immediately.
- Respect queue order: agents listed as waiting in the queue have priority when the resource becomes free.
- Clean up on exit: remove your presence entry and your claims. Delete the Team Lock file only when the presence log is fully empty.
User Locks are a separate, protected category. They lock a project durably, and
only the user (manually or via the watcher GUI) may remove them -- agents and
the stale-cleanup (prune_stale_locks.py) never touch them, even when nominally
expired.
LOCK.user.txt-- entire project, user-owned lock.LOCK.user.<scope>.txt-- component, user-owned lock.- The
usermarker segment is reserved (liketeam). Detection:lock_utils.is_user_lock(); protection:lock_utils.is_protected_lock()/is_prunable(). - Easiest via the watcher GUI ("Locks/Permissions" button) or the template with
removable_by: user. - Since v1.4.0, protected locks (user + condition) also never expire in
lock_utils.is_expired()-- previously a nominally expired user lock could incorrectly drop out ofactive_locks()/lock_scan.pyoutput.
Condition Locks (since v1.4.0) hold until a condition is fulfilled instead of until a time expires, and typically lock only specific operations instead of the whole project. Use case: "no release/upload of artifact X before the review follow-ups are done" -- while normal development/research on the project stays unrestricted.
LOCK.condition.txt-- condition-based lock at project level.LOCK.condition.<scope>.txt-- condition-based lock for a component, e.g.LOCK.condition.publish-release.txt.- The
conditionmarker segment is reserved (likeuser/team). Detection:lock_utils.is_condition_lock(); protection:is_protected_lock(). - No time expiry:
expires_afterhas no effect;prune_stale_locks.pyand bulk-unlock never touch condition locks.lock_scan.pyshows "until condition met: ..." instead of a remaining time. - Required field
release_condition:-- state precisely and verifiably WHAT must be done and WHERE it is documented. - Field
operations:(comma-separated) names the LOCKED operations (e.g.operations: publish-release, registry-upload). Everything not listed remains explicitly allowed. Without anoperationsfield the lock applies to the whole scope according to itsmode. Helper:lock_utils.locked_operations(path). - Release: unlike user locks, any agent may remove the lock once it has
verifiably fulfilled the
release_condition-- document the fulfilment in the project register when removing. If unsure whether the condition is met: do NOT remove; ask the user.
Agent-neutral, folder-scoped permission layer alongside the LOCK*.txt files --
readable by all agents (Claude, Codex, Gemini, Kimi).
Syntax borrowed from .claude/settings.json, but agent-wide and folder-scoped:
{ "format": "lock-permissions-v1", "default": "allow",
"rules": { "allow": ["Read(**)"], "deny": ["Bash(rm:*)", "Write(**/CREDENTIALS/**)"], "ask": ["Write(**)"] },
"applies_to_agents": ["claude","codex","gemini","kimi","*"] }- Patterns:
Tool(glob)(Bash(...),Read(...),Write(...)),mcp__vendor__tool,*. - Precedence:
deny > ask > allow > default. Evaluation:permissions.py::evaluate(perm, agent, action). - Enforcement = voluntary convention + GUI/audit (like
LOCK*.txt). Template:LOCK_PERMISSIONS_TEMPLATE.json.
bulk_lock.py sets/removes exclusive LOCK.txt across all connected top-level
roots (lock_roots.json) in one step:
bulk_lock(roots, commit=False)-- dry-run by default; idempotent (existing locks stay); created locks carrycreated_by: bulk(exact rollback via session manifest).bulk_unlock(...)-- removes onlycreated_by: bulklocks; never user locks.- CLI:
python bulk_lock.py lock|unlock --commit.
Tier 1 -- RESPECT (always required, everywhere):
Before modifying any project or component, check whether a non-expired
LOCK*.txt exists for the affected area (the project-wide LOCK.txt blocks
everything). If one exists and has not expired: do not touch it -- pick another
project or wait. This applies system-wide to every agent in every pipeline.
Tier 2 -- CREATE (optional unless mandated): Actively creating a lock at the start of work is not universally required.
- If a project marks itself with
LOCK-required: yesin its documentation, creating a lock is mandatory there. - Otherwise: recommended whenever parallel work is possible.
Fallback / precedence: This spec is the system-wide default. Project-specific rules take local precedence (more specific beats more general): a project may declare stricter requirements (e.g. mandatory creation) or use a custom scope name.
- RESPECT: check for an active lock on the area before working.
- CLAIM: create your own
LOCK.txtorLOCK.<scope>.txtfrom the template (owner, ISOcreated,expires_after,purpose). - RELEASE: delete the lock file you created when done. Active release
by the creator is required; the 24h expiry is only a safety net for
forgotten locks. If work takes longer, renew
createdso the lock does not expire prematurely.
Place all scripts in the same directory as lock_roots.json.
On Windows, always set PYTHONIOENCODING=utf-8 (cp1252 default encoding).
List active locks system-wide (read-only):
python lock_scan.py
python lock_scan.py --json
Remove expired locks:
# Preview first (deletes nothing):
python prune_stale_locks.py --dry-run
# Actually remove:
python prune_stale_locks.py
Refresh LOCK-CACHE.md:
python lock_scan.py --write-cache
Writes cache file(s) as defined in lock_roots.json ("caches" key).
Custom roots file:
python lock_scan.py --roots-file /path/to/my_roots.json
python prune_stale_locks.py --roots-file /path/to/my_roots.json
Scan performance: lock_roots.json controls default_max_depth (default 4),
shallow_depth (default 2, for roots with "shallow": true for large trees),
and skip_dirs (directories skipped including their subtrees, e.g.
node_modules, .venv, .git, build, releases).
lock_utils.py is the canonical format/scope/expiry library (incl. is_user_lock,
is_protected_lock, is_prunable). Import it from your own scripts rather than
re-implementing the logic. Companion modules: permissions.py (LOCK.permissions
evaluation) and bulk_lock.py (immediate lockdown / reversal).