Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions docs/adr/0026-other-working-directories-are-observed-never-touched.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# 0026: Other Working Directories Are Observed, Never Touched

Status: Accepted

Date: 2026-09-12

## Context

ADR-0022 gave a change its own working directory so two changes can run
at once, and ADR-0024 made what can run alongside what derivable rather
than declared. ADR-0025 drew the result as a picture. All three answer
for **one** working directory: the one the host was pointed at.

On 2026-09-12 two agents were working on this repository at once — one
in the primary checkout, one in `…​.worktrees/proposals` — and the owner
could see only their own. Six proposals existed a directory away and
were invisible from the tool built to show what is in flight.

Two things went wrong in that session that this ADR exists to stop
happening again, and neither was a bug in any code:

- The primary checkout was sitting on a branch whose pull request had
already merged, so the tool honestly reported an empty queue. The
owner could not tell "there is nothing to do" from "I am looking at a
stale checkout". Nothing on screen named the branch being read.
- A change that had been archived in one directory was still present, as
a tracked file, in another — inherited from the branch point, put
there by nobody. Harmless while untouched, and a genuine collision the
moment it is edited. Nothing would have reported it.

A working directory is not a remote thing. `git worktree` shares one
object store and one ref namespace: the sibling's `.git` is a file
naming a subdirectory of the primary's. Everything below is therefore a
local read, with no daemon, no network and no registry.

## Decision

**Other working directories are surveyed, and never acted on.**

The survey reports, per directory: the branch it has checked out, the
changes in its own `openspec/changes`, how far each one's tasks have
got, and — where a mutating run holds it — the lease's holder and git
author.

Read-only is a property of what the view can do, not of how it looks.
A foreign change carries no action: no opening it in the editor, no
starting a run against it, no ticking anything. Recessed styling
agrees with that fact; it does not create it.

The rule is specific because the failure is specific. A change's
identity is the pair `(working directory, name)`, not the name. Two
directories can hold a change of one name at different content — that
already happened here — and an action routed by name alone would act on
a different change while looking perfectly correct.

**No git is run against a directory this host does not own.**

Enumerating the working directories is one `git worktree list` against
this repository, which this host does own. Everything after that is a
filesystem read: names under `openspec/changes`, each `tasks.md`, each
`.openspec-ui/workspace.lease.json`.

The collision detection of ADR-0024 is deliberately not computed for a
foreign directory. It costs a git invocation per directory per read, and
it answers a question the viewer cannot act on: a collision between two
changes in somebody else's directory is not theirs to resolve. Work not
worth doing is not made cheap by moving it to the background — the
background changes who waits, not what is spent.

**A directory is labelled, and the label is not an identity.**

Each working directory carries a short label. It defaults to the
directory's own name — `proposals` — which is already chosen, already
meaningful, needs no configuration, and is distinct by construction
since git will not put two working directories at one path. It can be
overridden by a file in that directory.

The label lives in the directory and not only in the lease, because the
lease exists only while a mutating run holds one. An agent editing files
holds no lease, and that is the ordinary state — the survey has to work
then, which is most of the time.

The word is "label" and not "owner". "Owner" asserts authority, and this
view grants none; the lesson of ADR-0025's neighbour, `a-lease-says-who`,
is that a word which overstates gets believed. The label is
self-declared: attribution, never authentication.

**The git author is reported, and reported as different when it is.**

The lease already records the git identity of the directory that took
it. Where that identity differs from this checkout's own, the survey
says so — that is how a second *person* becomes visible, as opposed to a
second directory.

It is said in a word and agreed with by colour, never carried by colour
alone: this shell is held to WCAG AA, and a reader who cannot separate
two hues must still be able to tell whose run it is.

The two are different questions and neither stands in for the other. The
label answers "which working directory"; the git author answers "whose
commits would this be". On one person's machine every directory reports
the same author, and that is correct rather than a defect.

**Nothing foreign enters the local order.**

Each directory's changes are laid out on their own, by the layout of
ADR-0025, against that directory's own queue. They are never merged into
another directory's columns and no relation is drawn between directories.

A column means depth in a declared order. Between changes on two
branches the repository declares no order at all, so a line across would
assert a sequence that does not exist — the same reason a collision is
not an edge in ADR-0025, and believed for the same reason: it would look
like every other line in the drawing.

**A change in two directories at once is reported.**

Not prevented, and not resolved. It arises from ordinary branching — cut
a working directory from a main that has active changes and every one of
them comes along — so it is a condition to be noticed, not an error to
be refused.

## Consequences

The survey works with no configuration: a directory that declares
nothing still reports its name, its branch and its changes.

The tool can say which branch each reading came from, so an empty queue
is distinguishable from a stale checkout.

A foreign directory costs filesystem reads and no subprocess. It is read
less often than the local one and only while being looked at, on the
same reasoning ADR-0025 used for the tab itself.

The survey is a reading, never a subscription. A directory can change
between two of them, and the view says when it last looked.

## Alternatives considered

**A central registry or daemon agents report to.** Rejected: the
information is already on this disk, and a registry would add a second
source of truth that can disagree with the filesystem — and be believed,
because it is the one being displayed.

**Reading git in each foreign directory for a full readiness report.**
Rejected above: a subprocess per directory per read, for collisions the
viewer cannot act on.

**Merging every directory's changes into one graph.** Rejected: it would
place changes from unrelated branches in shared columns, asserting an
order the repository does not contain.

**Identifying a directory by its git author.** Rejected: on one person's
machine every directory reports the same author. It answers a different
question, and is kept for that question.

**Refusing to create a working directory that would duplicate an active
change.** Rejected for now: it would refuse the ordinary case (branching
from a main with work in flight) in order to prevent the rare one
(editing both copies). Reporting it costs nothing and forbids nothing.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ Alternatives / Consequences.
| [0023](0023-standalone-shell-visual-direction.md) | The standalone shell commits to an editor-native look | Accepted |
| [0024](0024-parallel-readiness-is-derived.md) | Parallel readiness is derived, never declared | Accepted |
| [0025](0025-the-pipeline-picture-is-derived-and-drawn-by-hand.md) | The pipeline picture is derived, and drawn without a graph library | Accepted |
| [0026](0026-other-working-directories-are-observed-never-touched.md) | Other working directories are observed, never touched | Accepted |

New architecture-impacting changes must add an ADR and reference it from the related OpenSpec change.
8 changes: 8 additions & 0 deletions openspec/changes/what-the-others-are-doing/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
schema: spec-driven
created: 2026-09-12
follows:
- changes-run-side-by-side
- what-can-start-now
- a-lease-says-who
blocked_by:
- a-graph-of-what-is-running
134 changes: 134 additions & 0 deletions openspec/changes/what-the-others-are-doing/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Design

See `docs/adr/0026-other-working-directories-are-observed-never-touched.md`
for the decisions: a directory is surveyed and never acted on, no git is
run against one this host does not own, a directory carries a label that
is not an identity, and nothing foreign enters the local order.

## Decision: a survey, not a second readiness report

`readChangeReadiness` derives facts that cost work: which changes
collide over a capability, which branches have changed the same files,
what can be started alongside what. That is right for the directory a
person is working in, where every answer is actionable.

For a directory somebody else is working in, none of it is. A collision
between two of their changes is not this viewer's to resolve, and
computing it costs a git invocation per directory on every read.

So the survey is a different, smaller thing: what a directory *is*,
rather than what could be done about it. Names, tasks, branch, lease.
The two are separate functions and the foreign one is not a degraded
version of the local one — it answers a different question.

## Decision: what is read, and what is never inferred

Read:

- the working directories, from one `git worktree list` against this
repository — the only git this feature runs;
- each directory's branch and head, from the same output;
- the names under its `openspec/changes`, excluding `archive`;
- each change's `tasks.md`, for how many items are closed;
- its `.openspec-ui/workspace.lease.json`, where one exists.

Never inferred: whether an agent is working, stuck, or idle. A directory
with no lease means no mutating run holds it right now — an agent
editing files holds none. Reporting that as "idle" would be the same
mistake as calling a git author a user: a word the data does not
support, believed because it was displayed.

## Decision: the label, and why it lives in the directory

Each directory has a short label, defaulting to the last segment of its
own path. That default is free and already meaningful: somebody chose
`proposals` when they made it, and git guarantees no two working
directories share a path, so labels are distinct by construction.

It is overridable by `.openspec-ui/worker.json` in that directory — one
field, self-declared.

It lives in the directory rather than in the lease because the lease
exists only while a mutating run holds one. The ordinary state of a
directory is somebody editing files in it, holding nothing. The survey
has to name it then, which is most of the time.

## Decision: "label", not "owner"

"Owner" asserts authority over the thing named, and this view grants
none — it cannot act on a foreign change at all. `a-lease-says-who`
already found that a word which overstates gets believed: the lease says
"git author" and never "user", so that a self-declared string is not
read as an established identity. The same applies here.

## Decision: the git author answers a different question

The label says which working directory. The git author says whose
commits a run there would be. Neither substitutes for the other: on one
person's machine every directory reports the same author, and that is a
correct answer, not a failure of the label.

Where a directory's lease records an author different from this
checkout's own configured identity, the survey says so. That is the
signal that a second *person* — not merely a second directory — is at
work.

It is said in a word, and colour agrees with the word. Never colour
alone: the shell is held to WCAG AA by a browser suite that runs axe,
and the same rule already governs running-versus-blocked on a pipeline
card.

## Decision: each directory keeps its own picture

Each directory's changes are laid out by `layoutChanges` against that
directory's own queue, and drawn as their own picture.

They are not merged into one graph. A column means depth in a declared
order, and between changes on two branches the repository declares no
order at all — placing them in shared columns would assert one. For the
same reason no relation is drawn between directories: a line across
would be believed, because it would look like every other line.

## Decision: a duplicate is reported, not refused

A change can exist in two directories at once. It arises from ordinary
branching — cut a directory from a main that has active changes and all
of them come along — so refusing it would refuse the common case to
prevent the rare one.

While both copies are untouched it is inert: the archive of one side
merges cleanly against an unmodified other. It becomes a collision the
moment either is edited, which is exactly when somebody wants to know.
So it is surfaced, and nothing is done about it.

## Non-Goals

Acting on another directory: opening, running, ticking, releasing its
lease. All of it is refused by construction — there is no control.

A registry, a daemon, or any reporting protocol between agents. The
information is on this disk already, and a second source of truth would
be free to disagree with the filesystem and be believed.

Directories of other repositories, or on other machines. `git worktree
list` bounds this to working directories of this repository.

Merging foreign changes into the local readiness report, which would
make `openspec-ui-cli ready` answer for work its caller cannot start.

## Risks / Trade-offs

The survey is a reading, not a subscription: a directory can change
between two of them. It says when it last looked, as the pipeline tab
already does.

A directory can be on a network path or an unmounted drive, where a read
blocks or fails. One unreadable directory must be reported as unreadable
and must not remove the others from the survey.

Reading another directory's files means displaying content this host did
not write. It is displayed as text and never executed — the same footing
as repository file contents everywhere else in this tool.

Many directories make a long page. They are read less often than the
local one and only while being looked at.
69 changes: 69 additions & 0 deletions openspec/changes/what-the-others-are-doing/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# What the others are doing

## Why

On 2026-09-12 two agents worked on this repository at once — one in the
primary checkout, one in a working directory beside it — and the owner
could see only their own. Six proposals sat one directory away, written
and invisible.

The tool answered honestly and uselessly. It reported the queue of the
directory it was pointed at, which happened to be sitting on a branch
whose pull request had already merged, so it said there were no active
changes. Nothing on screen named the branch it had read, so "there is
nothing to do" and "you are looking at a stale checkout" looked
identical.

A second thing went unnoticed in the same session: a change archived in
one directory was still present, as a tracked file, in another. It came
along with the branch point, put there by nobody. Harmless while
untouched, and a real collision the moment either copy is edited.
Nothing reports it today.

The parts to fix both are already here. `changes-run-side-by-side` gave
each change a working directory; `what-can-start-now` derived what is
running from the lease each one holds; `a-lease-says-who` made the lease
say whose run it is; `a-graph-of-what-is-running` drew the result. Every
one of them stops at the edge of the directory it was given.

Nothing about that edge is expensive to cross. A working directory is
not a remote thing: `git worktree` shares one object store, and the
sibling's `.git` is a file naming a subdirectory of this one. The survey
is a local read, with no daemon, no network and no registry.

## What Changes

- A **survey** of every working directory of this repository: its label,
the branch it has checked out, the changes in its own queue, how far
each one's tasks have got, and who holds it where a run does.
- The pipeline tab shows each directory's changes as their own picture —
this one at full strength, the others recessed beneath it. Nothing
foreign enters the local order, and no relation is drawn between
directories.
- Foreign changes carry **no action at all**. Read-only is what the view
can do, not how it looks.
- Each directory carries a **label**, defaulting to its own directory
name and overridable by a file in it. Self-declared: attribution,
never authentication.
- Where a directory's lease records a **git author different from this
checkout's**, the survey says so — in a word, agreed with by colour,
never carried by colour alone.
- A change present in more than one directory at once is **reported**,
not prevented and not resolved.
- The tab **names the branch each reading came from**, so an empty queue
is not mistaken for a stale checkout.
- **No git is run against a directory this host does not own.** One
`git worktree list` enumerates them; everything after that is a
filesystem read.

## Impact

- `packages/core` — a survey module beside `change-readiness`, reading
directories rather than deriving new facts about them.
- `packages/server` — one endpoint carrying the survey.
- `packages/webui` — the foreign bands under the local picture, and the
branch each reading names.
- No new dependency, no background service, no configuration required
for a directory to appear.

See `docs/adr/0026-other-working-directories-are-observed-never-touched.md`.
Loading
Loading