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.
pure-locking/watcher/ provides an optional local daemon, REST API, and browser
UI. It uses the same scripts and config from pure-locking/:
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 pure-locking/watcher/lock_watcher.py --update-cache
python pure-locking/watcher/web_server.py --port 8095Windows shortcut:
pure-locking\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: pure-locking/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 pure-locking/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:permission-control/permissions.py::evaluate(perm, agent, action). - Enforcement = voluntary convention + GUI/audit (like
LOCK*.txt). Template:permission-control/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.
"Two Tiers of Enforcement" already names the problem: two agents starting at the same time both see an empty folder. Solved so far is only the case where one was demonstrably first. With cloud sync at 30 s – 5 min latency both see an empty folder, both create, and both consider themselves the holder. This is not an edge case: scheduled automations start on several hosts at the same clock time.
Stage 1 — exclusive creation (always on). lock_create.py creates the lock
file exclusively (open("x")) instead of checking and then writing. Between the
check and the write there used to be a window in which a second process creates
the same file. --force still overwrites deliberately.
Stage 2 — contest procedure (attempted when it pays off).
1. CREATE write the lock exclusively
2. QUARANTINE wait (default 300 s) for the sync to align both views
3. RECHECK read the directory again
4. DECIDE earliest `created` wins; exact tie -> lexicographically smaller host
5. LOSER remove own lock (exit code 3), release the area
Both sides compute the same result from the same files — no server, no database, no real-time channel. The wait is the mechanism, not decoration: without it the recheck reads the same unsynchronised view as before.
When it runs. Not always. Minutes of lead time are expensive for a short edit
and cheap for an automation run that then works for hours. should_contest()
therefore asks two questions: is the area in a cloud folder at all, and is this an
automation? Only then.
This deliberately inverts a common reflex. When cloud pressure is detected (e.g. via the placeholder attributes that FileCommander also inspects), tools often skip the work entirely to avoid conflicts. That is safe but expensive — work that could have happened does not. A protocol is the better answer than abstention: attempt it rather than give up.
Cloud detection, three stages (contested.cloud_pressure): an optional
external prober (e.g. a FileCommander call) is asked, never required; otherwise
the Windows placeholder attributes (FILE_ATTRIBUTE_OFFLINE, RECALL_ON_OPEN,
RECALL_ON_DATA_ACCESS, reparse point); otherwise known cloud folder names in the
path.
What the decision rule applies to. It needs two visible claims — that means team locks: they carry the host in the name, so they coexist in the directory, and they act like an exclusive lock towards foreign systems. For equal or overlapping scopes it was previously undefined who wins. Exclusive locks carry no host; there Stage 1 applies, and on true simultaneity the cloud service produces a conflict copy that stays visible instead of silently overwriting. User and condition locks are never overruled — protected categories do not take part.
Scope overlap (contested.scopes_overlap): a project lock overlaps
everything; otherwise containment with a segment boundary — assets overlaps
assets.images but not assets-backup. A mere character prefix is not an
ancestor relation.
Time resolution: created is now written with seconds
(%Y-%m-%dT%H:%M:%S). At minute granularity two near-simultaneous claims
regularly land in the host tiebreak, where the same host loses structurally every
time. lock_utils._parse_created has always read seconds ("seconds optional"), so
this is not a format break.
An expired own lock never wins. Other systems filtered it out long ago; without that check it would see itself as the earliest claim while nobody else still sees it — two winners against identical data, with no sync delay involved.
Honest limit: if a second system starts after the first one's quarantine has elapsed but before its lock has synced, the rule does not apply. The residual risk is in the range of seconds instead of, as without the procedure, a total failure of coordination. Hard exclusion would require a shared transactional instance.
Invocation:
python lock_create.py <project> --team <HOST> # procedure on demand
python lock_create.py <project> --team <HOST> --contested # force it
python lock_create.py <project> --team <HOST> --no-contest # suppress it
python lock_create.py <project> --team <HOST> --quarantine 120
Exit code 3 = claim lost, own lock removed.
Origin: the procedure comes from ellmos-ai/system-auditor (audit host lock,
2026-08-15). There exclusion was unwanted — parallel audits are the product —
which is why it moved here, where exclusion is the purpose.
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).
PRIVATE.txt / PUBLIC.txt — Veröffentlichungs-Locks [U 2026-07-18, Rückspiegelung aus _scripts/LOCK-SYSTEM.md]
Zwei besondere Lock-Arten für die VERÖFFENTLICHUNG (nicht Bearbeitung) von
Repositories/Projekten; kanonische Vollfassung: OneDrive/_scripts/LOCK-SYSTEM.md.
PRIVATE.txt im lokalen Projekt-/Repo-Root = Projekt darf NICHT public
werden (GitHub-Visibility, Forks/Mirrors, Registries, Zenodo/Preprints,
öffentliche Doku). Aufhebung: LEERE Datei → nur der User entfernt sie, kein
Verfall, kein prune. MIT INHALT können Blocker/Aufhebebedingungen definiert
sein — sind alle nachweislich erfüllt, darf auch ein LLM/Agent die Datei
löschen (mit dokumentiertem Nachweis); ohne explizite Bedingungen gilt sie
wie leer. Pflichtprüfung vor gh repo create --public, --visibility public, npm publish, Registry-Submits; GithubBot//repo-publish-check
behandeln solche Projekte als „nie public stellen/vorschlagen".
PUBLIC.txt = Veröffentlichung FREIGEGEBEN (auch wenn noch nicht
public); kann Aufträge enthalten, die MIT der Veröffentlichung zu erledigen
sind (Registries, Banner, llms.txt, Release-Tag …). Anlage nur durch den
User oder auf dokumentierte User-Freigabe.
Gemeinsame Regeln: Beide Dateien sind lokale Steuerdateien und werden
NICHT committet (in Projekt-.gitignore eintragen; Bots prüfen den lokalen
Projektspiegel). Konflikt beider Dateien → fail-closed PRIVATE.txt gilt,
User informieren. Bewusst außerhalb des LOCK*.txt-Scanmusters von
lock_scan.py (Publikations-, keine Bearbeitungssperre). Anlass: ein
internes Repository wurde ohne User-Freigabe public angelegt und am
2026-07-18 auf privat zurückgestellt.