---
title: "MGS3007: project lock held by an ancestor run"
description: Fires when a magus run cannot proceed because a project it must lock is already locked by one of its own ancestor invocations, which cannot finish until this run does.
tags: [MGS3007, locks, nested run, deadlock, project lock, magus.run]
---

# MGS3007: project lock held by an ancestor run

A magus run tried to lock a project that one of its own ancestor invocations
already holds. The ancestor cannot release it, because the ancestor is blocked
waiting for this run to exit, so the run is refused instead of queued.

```text
[MGS3007] project . is locked by the magus run this one is nested inside
  (pid 42636 (magus run screenshots .)), which cannot finish until this one does.
  Waiting would never end, so it is refused instead.
```

## Why

A run takes the EXCLUSIVE workspace lock for every project it selects and holds
it for the whole invocation. That lock exists to stop two magus invocations from
mutating one project at once, and for two unrelated runs, waiting is exactly
right: the holder finishes, the lock frees, the waiter proceeds. The wait is
announced up front and reprinted every 15 seconds.

Nesting breaks that reasoning. When the holder is an ancestor of the waiter, the
holder is inside `os.exec`/`magus.run`, waiting for this very process to exit.
Neither side can move. flock cannot tell this apart from ordinary contention, and
neither can a timeout - a timeout only converts a hang into a late failure that
still does not say why. magus carries each invocation's ancestry
(`MAGUS_INVOCATION_ANCESTORS`, inherited like `MAGUS_LEVEL`, and recorded in the
lock's owner sidecar) so the two cases can be told apart at the moment of
acquisition.

The usual shape is a target that shells out to magus against its own project:

```buzz
export fun deploy(ctx: magus\Context, args: [str]) > void {
    magus\run(["release"]);   // no project selector: the project this run holds
}
```

Every spawn that goes through magus's own subprocess layer is covered:
`os.exec("magus", ...)`, `magus.cmd("run", ...)`, `magus.run(...)`, and a shell
script several levels down all inherit the ancestry and all raise this. A magus
started by something that does NOT inherit that environment - a long-running
service process, a detached daemon - is not covered, and still waits.

## Resolution

1. **Express the dependency instead of spawning one.** A target that needs
   another target's work wants an edge, not a subprocess:

   ```buzz
   export fun deploy(ctx: magus\Context, args: [str]) > void {
       ctx.needs(release);
   }
   ```

   One invocation then runs both, under one lock, with the cache and the
   affected-set tracking intact - none of which a nested process inherits.

2. **Target a project the outer run does not hold.** Nesting itself is fine when
   the project sets are disjoint: a root target may run `magus\run(["build",
   "console"])` while holding only the root lock. Name the project explicitly so
   the selection cannot widen to the one the outer run holds - a bare
   `magus\run(["build"])` selects by directory and can pick up the caller's own
   project.

3. **Reach for a read-only subcommand.** `magus ls`, `describe`, `graph`, and
   `query` take no project lock, so a target may call them freely. The typed
   in-process members (`magus\projects`, `magus\targets`, `magus\affected`,
   `magus\graph`) are better still: they answer from the workspace already open
   and never fork.

4. **If the work genuinely belongs in a script, keep it in the script.** A
   sequence of magus commands driven by a shell script is a valid thing to have;
   it just cannot be wrapped in a target that the commands then contend with. Run
   the script directly.

## See also

- [`ctx.needs`](../../../concepts/targets.md): the dependency edge to use instead of a nested run.
- `magus status`: names the lock's holder and every waiter, including this case.
- [MGS5004](../services/MGS5004.md): the other code about a run that cannot proceed in the process context it was given.
