Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Execution Locks

Use [locks] to serialize operations that share build output, a test database, a deployment endpoint, or another exclusive resource. Permissions decide whether a call may run; locks decide when an authorized call may run.

[permissions]
approve = ["exec(cargo) args(build)"]

[locks]
build-output = [
    "exec(cargo) args(*)",
    "exec(make) args(*)",
    "shell(make check)",
    "background(make check)",
]

cargo build is approved by this example. Another Cargo invocation still needs approval, but every authorized Cargo invocation takes build-output. Session approvals, remembered approvals, and --yes do not bypass resources. Denied and malformed calls acquire nothing. When a call needs approval, its permission dialog or plain-text prompt lists every selected name as Locks if approved: before the call. The preview acquires nothing and is omitted when no names match; approved execution uses the same selected set.

Names and matching

Names match [a-z][a-z0-9_-]{0,63}. An absent or empty table acquires nothing; an empty pattern array selects no calls. Invalid names and patterns fail config loading rather than being ignored.

Patterns use the permission syntax. Direct exec calls pin the program and can glob the space-joined arguments. Shell and background patterns are exact command strings, without wildcards. Bare tool names select every call of that tool. Every matching resource applies; there is no first-match precedence.

exec(cargo) does not match shell(cargo build). shell(make check) does not match background(make check). Match the outer operation, not subprocesses or commands hidden inside scripts. Ordinary tools can have lock patterns too.

Gates, including final publication gates, match by their command form. Arrays such as command = ["cargo", "test"] match exec(cargo) args(test) and exec(cargo) args(*). Strings match shell(<expanded command>). For a gate with {files}, matching uses the final argument vector or shell-quoted expansion. Bare shell conservatively covers all shell operations, including string gates and user commands, but not array gates. There is no separate gate-lock field.

Within one gate run, a successful gate keeps resources also needed by the next executed gate. Skipped gates do not break retention. Other resources release before that next gate; failure, cancellation, and the end of the run release all resources after cleanup. If an additional resource is busy, heph releases the retained set and retries the complete set to avoid deadlocks. This applies to acceptance and final publication gates, not across separate runs or retries.

Scope and configuration

Resources belong to the canonical original project, across all its refs, worktrees, and heph processes using the same data root. Symlink aliases coordinate. Distinct projects, clones, or data roots do not. A user-level rule applies independently in each project, not globally.

User and project maps merge by name: project patterns append to user patterns, and identical entries are deduplicated. An empty project array cannot remove a user requirement. Remembering an approval never modifies this table.

Heph reads the authoritative config files at startup. Restart to change lock policy. A selected set stays fixed while its operation waits and runs.

Waiting and ownership

Contention adds waiting for lock build-output to scrollback, naming the resource that actually blocked acquisition and its last recorded owner when available. Owner records include the conversation name and operation (including job or gate identity). If the blocker or recorded owner changes, another wait line appears. Metadata can be stale or briefly absent; it is not proof of ownership.

The bottom status bar shows locks: worktree, build-output while those locks are held. worktree is the session's worktree lease; publication appears while it owns publication. Named execution resources stay listed through cleanup, including background jobs after a turn ends. Long lists are shortened with ...; scrollback retains full acquisition and release names and timings. Publication contention also names the holding session, falling back to its PID for older lock records, and scrollback records acquisition and release.

Waiting is interruptible and has no automatic timeout. Command and gate deadlines begin after acquisition. Input, scrolling, and unrelated operations remain responsive. Esc or Ctrl-C asks before aborting a queued or running user command.

Resources remain held through command execution and supervised cleanup. Background jobs keep their resources after returning a job ID. If a later call waits on one, inspect or stop that job; /job-kill ID requests cancellation without acquiring resources. A configured job_status or job_kill call whose selected resources overlap its target job fails instead of waiting on itself.

Lock files live at locks/<project-hash>/<name>.lock under the heph data root. They are persistent OS advisory locks: do not delete them to release ownership. Owner metadata is diagnostic, potentially stale, and never proves a lock is free. Acquisition failures refuse execution; there is no unlocked fallback.

A dedicated Linux supervisor owns locked commands and their descriptors. It terminates and reaps descendants before releasing resources, including when its heph owner crashes or drops the control channel. Forcibly killing the supervisor itself, or deliberately escaping supervision, is outside this guarantee. Detached writers are not supported; use a managed background job. Replacing the installed heph binary does not break supervisor startup in an existing session: supervisors continue using that session's running version. Restart heph to use the new version.

This is advisory coordination among heph operations, not a sandbox. Ordinary terminal commands do not participate. Serializing operations prevents overlap; it does not fix sequential cache-freshness errors or provide per-ref artifacts. There are no capacities, shared modes, global resources, or external wrapper.