diff --git a/docs/adr/0026-other-working-directories-are-observed-never-touched.md b/docs/adr/0026-other-working-directories-are-observed-never-touched.md new file mode 100644 index 0000000..f1e9410 --- /dev/null +++ b/docs/adr/0026-other-working-directories-are-observed-never-touched.md @@ -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. diff --git a/docs/adr/README.md b/docs/adr/README.md index 4291a17..b2f3183 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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. diff --git a/openspec/changes/what-the-others-are-doing/.openspec.yaml b/openspec/changes/what-the-others-are-doing/.openspec.yaml new file mode 100644 index 0000000..6a1dda3 --- /dev/null +++ b/openspec/changes/what-the-others-are-doing/.openspec.yaml @@ -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 diff --git a/openspec/changes/what-the-others-are-doing/design.md b/openspec/changes/what-the-others-are-doing/design.md new file mode 100644 index 0000000..d745ad2 --- /dev/null +++ b/openspec/changes/what-the-others-are-doing/design.md @@ -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. diff --git a/openspec/changes/what-the-others-are-doing/proposal.md b/openspec/changes/what-the-others-are-doing/proposal.md new file mode 100644 index 0000000..c66ba67 --- /dev/null +++ b/openspec/changes/what-the-others-are-doing/proposal.md @@ -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`. diff --git a/openspec/changes/what-the-others-are-doing/specs/shared-ui/spec.md b/openspec/changes/what-the-others-are-doing/specs/shared-ui/spec.md new file mode 100644 index 0000000..3e0df9c --- /dev/null +++ b/openspec/changes/what-the-others-are-doing/specs/shared-ui/spec.md @@ -0,0 +1,133 @@ +## ADDED Requirements + +### Requirement: Every working directory of the repository is surveyed + +The tool SHALL report every working directory of the repository it was +opened on, not only the one it was pointed at. + +For each, it SHALL report the branch that directory has checked out, the +changes in that directory's own queue, how far each of those changes has +got, and, where a mutating run holds it, who holds it. + +A directory that cannot be read SHALL be reported as unreadable, and +SHALL NOT remove the others from the survey. + +#### Scenario: A second working directory with changes of its own + +- **WHEN** another working directory holds changes that this one does + not +- **THEN** they are reported, with the branch they are on + +#### Scenario: A directory that cannot be read + +- **WHEN** one working directory cannot be read +- **THEN** it is reported as unreadable and the rest of the survey still + appears + +### Requirement: Another directory's changes cannot be acted on + +A change belonging to another working directory SHALL carry no action: +it SHALL NOT be openable, runnable, or modifiable from here. + +A change SHALL be identified by its working directory together with its +name, never by name alone, so that no action can reach a change of the +same name in a different directory. + +Recessed or otherwise quietened presentation SHALL NOT be the only thing +that makes it unmodifiable. + +#### Scenario: A change belonging to somebody else's directory + +- **WHEN** a change from another working directory is shown +- **THEN** nothing offers to open, run, or change it + +#### Scenario: One name in two directories + +- **WHEN** two working directories each hold a change of the same name +- **THEN** each is shown under its own directory, and neither can be + acted on through the other + +### Requirement: A working directory is labelled, and the label is not an identity + +Each working directory SHALL carry a label. + +Where the directory declares none, its own directory name SHALL be used, +so that a directory that declares nothing is still named. + +The label SHALL be reported as self-declared. Nothing SHALL be permitted +or refused on the strength of it. + +#### Scenario: A directory that declares nothing + +- **WHEN** a working directory carries no declared label +- **THEN** it is named by its directory name + +#### Scenario: A directory that declares a label + +- **WHEN** a working directory declares a label +- **THEN** that label names it + +### Requirement: A run by a different git author is distinguished + +Where a working directory is held by a run whose recorded git author +differs from this checkout's own configured identity, the survey SHALL +say so. + +It SHALL be said in words. Colour MAY agree with those words and SHALL +NOT be the only thing that carries the distinction. + +The label and the git author SHALL be reported as the separate facts +they are; neither SHALL stand in for the other. + +#### Scenario: Another person's run + +- **WHEN** a directory's lease records a git author other than this + checkout's +- **THEN** the survey says so in words + +#### Scenario: Several directories, one person + +- **WHEN** every directory records the same git author +- **THEN** none is distinguished as another person's, and each is still + named by its own label + +### Requirement: A reading names the branch it came from + +The tool SHALL name the branch each reading was taken from. + +An empty queue SHALL therefore be distinguishable from a reading taken +somewhere other than where the reader expected. + +#### Scenario: A checkout with no active changes + +- **WHEN** the directory being read has no active changes +- **THEN** it says so and names the branch it read + +### Requirement: Nothing from another directory enters this one's order + +Changes belonging to another working directory SHALL be laid out against +that directory's own queue, and SHALL NOT be placed in this one's order. + +No relation SHALL be drawn between changes in different working +directories. The repository declares no order between them, and a drawn +relation would assert one. + +#### Scenario: Two directories with unrelated changes + +- **WHEN** two working directories each hold changes +- **THEN** each set is laid out on its own, and nothing is drawn between + them + +### Requirement: A change present in more than one directory is reported + +Where one change exists in more than one working directory, the survey +SHALL report it. + +It SHALL NOT be refused, prevented, or resolved: it arises from ordinary +branching, and becomes a conflict only if a copy is edited. + +#### Scenario: A change inherited by a new working directory + +- **WHEN** a change exists in two working directories at once +- **THEN** the survey reports that it does, and both continue to be + shown diff --git a/openspec/changes/what-the-others-are-doing/tasks.md b/openspec/changes/what-the-others-are-doing/tasks.md new file mode 100644 index 0000000..7ef5dba --- /dev/null +++ b/openspec/changes/what-the-others-are-doing/tasks.md @@ -0,0 +1,115 @@ +Two agents worked on this repository at once and the owner could see one +of them. The tool reported the queue of the directory it was pointed at, +which was sitting on a branch whose pull request had already merged, so +it said there was nothing to do — and nothing on screen named the branch +it had read. + +## 1. The survey + +- [ ] 1.1 `worktree-survey.ts` in core: every working directory of this + repository, each with its path, label, branch, head, changes, and the + lease holder where one exists. +- [ ] 1.2 One `git worktree list` and nothing else. No git is run against + a directory this host does not own — see ADR 0026. Everything after + that is a filesystem read. +- [ ] 1.3 Per directory: the names under `openspec/changes` other than + `archive`, and each one's closed and total task counts. +- [ ] 1.4 The lease is read where present. Absent means no mutating run + holds it — never "idle", which is a claim the data does not make: an + agent editing files holds no lease. +- [ ] 1.5 A directory that cannot be read is reported as unreadable and + does not remove the others. A network path or an unmounted drive is + one directory's problem, not the survey's. +- [ ] 1.6 Collisions are deliberately NOT computed for a foreign + directory: a git invocation per directory per read, answering a + question the viewer cannot act on. +- [ ] 1.7 A change present in more than one directory is reported as + such. Not refused and not resolved — it comes from ordinary branching. + +## 2. The label + +- [ ] 2.1 Defaults to the last segment of the directory's own path. Free, + already meaningful, and distinct by construction: git will not put two + working directories at one path. +- [ ] 2.2 Overridable by `.openspec-ui/worker.json` in that directory, + one field. +- [ ] 2.3 Lives in the directory and not only in the lease. The lease + exists only while a mutating run holds one, and the ordinary state is + somebody editing files, holding nothing. +- [ ] 2.4 Called a label, never an owner. "Owner" asserts authority this + view does not grant, and `a-lease-says-who` already found that a word + which overstates gets believed. Self-declared: attribution, never + authentication. + +## 3. Whose run it is + +- [ ] 3.1 The survey marks a directory whose lease records a git author + different from this checkout's own configured identity. +- [ ] 3.2 Said in words; colour agrees with the words and never carries + the distinction alone. The browser suite runs axe at WCAG AA, and the + same rule already governs running-versus-blocked on a card. +- [ ] 3.3 The label and the git author are reported as separate facts. + On one person's machine every directory reports the same author, which + is a correct answer rather than a failure of the label. + +## 4. Getting it to the shell + +- [ ] 4.1 A server endpoint carrying the survey, in the shape core + returns. +- [ ] 4.2 A webui client, browser-safe: it asks, it does not derive. + `packages/server/src/static.test.ts` is the gate. + +## 5. Showing it + +- [ ] 5.1 The pipeline tab names the branch its reading came from, so an + empty queue is not mistaken for a stale checkout. This is the one item + that would have saved the session that prompted this change. +- [ ] 5.2 Each foreign directory gets its own picture beneath the local + one, laid out by `layoutChanges` against that directory's own queue. +- [ ] 5.3 Recessed, and labelled with its label, its branch, and — where + a run holds it — the holder and git author. +- [ ] 5.4 No relation is drawn between directories. The repository + declares no order between them, and a line would be believed because + it would look like every other line. +- [ ] 5.5 A foreign change carries no action: no opening, no running, no + ticking. Read-only is what the view can do, not how it looks. +- [ ] 5.6 A duplicate change is called out where it appears. +- [ ] 5.7 Foreign directories are read less often than the local one, and + only while the tab is being looked at. + +## 6. Tests + +- [ ] 6.1 Core: two working directories, each with its own changes; both + surveyed with their branches. +- [ ] 6.2 Core: a directory with no lease is reported as held by nobody, + and never as idle. +- [ ] 6.3 Core: an unreadable directory is reported as such and the rest + of the survey survives. +- [ ] 6.4 Core: the label defaults to the directory name and a declared + one overrides it. +- [ ] 6.5 Core: one change in two directories is reported as duplicated. +- [ ] 6.6 Core: no git subprocess is invoked per foreign directory — + asserted against a wrapper that records its calls, because this is a + cost the code could reacquire silently. +- [ ] 6.7 webui: a foreign change offers no action, and a foreign + directory whose author differs says so in words rather than only in + colour. +- [ ] 6.8 webui: no relation is drawn between two directories. +- [ ] 6.9 Browser suite: the tab with a second working directory passes + axe at WCAG AA, and its screenshot under `docs/images/standalone/` is + regenerated. + +## 7. Verification + +- [ ] 7.1 This change validates strictly. `check(validate-change)` +- [ ] 7.2 `npm run verify` unpiped, after the last edit, with everything + staged. Record the run and the per-package test counts. +- [ ] 7.3 The whole browser suite, not a selected spec. +- [ ] 7.4 A pending changeset exists. `check(changeset-present)` +- [ ] 7.5 **Delegated to `claude-cli`**: with a second agent actually + working in a second working directory, open the tab and check that its + label, branch and changes are the ones on disk there, that nothing + offers to act on them, and that a run held there names its git author. + Evidence: both directories, and the tab. The unit tests drive the + survey with directories a test made; only a real second agent shows + that what it writes is what the survey reads.