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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ BenchmarkDotNet.Artifacts/
# docs-query's index (tools/DocsQuery): one file, fingerprinted, rebuilt when the docs change
/.docs-query.cache
/.docs-query.cache.*.tmp

# The desktop app's own worktree sessions (design/worktrees.md); worktrunk's go beside the repository
.claude/worktrees/
12 changes: 7 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ An agent builds and tests through `tools/BuildVerdict` (`build [<project|slnx>]`
verdict: the exit code named, each diagnostic once with its remedy from
`tools/BuildVerdict/build-verdict-remedies.tsv` (give an UNMAPPED code a row once its fix is known), and the
failing tests; the full log is `$GIT_DIR/build-verdict-last.log`. In an agent's Bash tool, `tools/BashGuard`
refuses the raw `dotnet build`, `dotnet test` and `dotnet msbuild` and prints the verdict's form of the command;
the raw commands above are for a terminal. The tools run from `bin/Release`, built at session start in the
refuses the raw `dotnet build`, `dotnet test` and `dotnet msbuild` and prints the verdict's form of the command,
and refuses `wt merge` and `wt step squash|rebase|push`; the raw commands above are for a terminal. The tools run from `bin/Release`, built at session start in the
session's own checkout ([decision 105](design/adr/0105-every-tool-is-built-in-the-session-s-checkout.md)).
A tool built from other sources refuses to run and names the command that rebuilds it,
`dotnet build tools/<Name> -c Release`.
Expand Down Expand Up @@ -113,8 +113,8 @@ here, so it leads and the source decides.
change" from it alone.
- **Refresh it**, since it does not follow the working tree: `index_repository` with the repository root
as `repo_path` and `name: "CodoMetis.TypeKit"`, at the start of a session and after a branch switch, a
rebase, a merge or a pull. A worktree under `.claude/worktrees/` isn't indexed: index it under a name of
its own and delete that project when done, or search the source.
rebase, a merge or a pull. A worktree under `.claude/worktrees/` or `../CodoMetis.TypeKit-worktrees/` isn't
indexed: index it under a name of its own and delete that project when done, or search the source.

The way into the docs is docs-query (`dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll`,
[its README](tools/DocsQuery/README.md)): `entry`, `cites`, `cited-by`, `list`, `claims` and `check`
Expand Down Expand Up @@ -215,7 +215,9 @@ What trips agents here:
`spike:`. A `fix:` ends with the trailers `Defect-Class:` and `Found-By:`, one of each, from the
tables of [decision 94](design/adr/0094-topic-branches-into-a-release-branch-and-fix-trailers.md)
(`CommitTrailerTests`). Work happens on a `<type>/<slug>` branch from `release/<version>`, reaches it
by a PR, and every merge is a merge commit: never squash or rebase a pushed branch.
by a PR, and every merge is a merge commit: never squash or rebase a pushed branch. Parallel work gets
a worktree each, made by `wt switch -c <type>/<slug> --no-cd` beside the repository, with the session
started in it ([design/worktrees.md](design/worktrees.md), [decision 126](design/adr/0126-parallel-work-runs-in-worktrunk-worktrees-beside-the-repository.md)).
7. **Hold what the documents promise** ([decision 112](design/adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md),
[test/oracles/README.md](test/oracles/README.md) for the files and how to add an oracle). A new public member,
or a new promise in `docs/` or a README, gets a claim in `test/oracles/claims.json`, quoted verbatim and naming
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ Work for the next version goes into its release branch, `release/<version>` (`re
into `main` ([decision 94](design/adr/0094-topic-branches-into-a-release-branch-and-fix-trailers.md)).
Branch from it as `<type>/<slug>`, where the type is your main commit's (`feat/…`, `fix/…`, `docs/…`,
`build/…`), and open the PR against it. PRs are merged with a merge commit, never squashed or rebased:
the design records cite commits by hash.
the design records cite commits by hash. To work on several at once, give each its own worktree beside
the repository: [design/worktrees.md](design/worktrees.md) has the commands.

- One concern per PR, closing one issue, with its labels and milestone ([above](#issues-and-labels)).
The description follows the template: `Closes #n`, then Problem, Change and Tests, with the reasoning
Expand Down Expand Up @@ -222,7 +223,8 @@ The packages share one version and are released together, only through
`## <version> — Unreleased` section. `ChangelogTests` fails while the baseline trails the last
version the changelog dates, and pack while the new section is empty, so open it with its first
line. Once that is on `main`, cut the next `release/<version>` from it and
push it, and open the version's milestone and its `epic` issue.
push it, open the version's milestone and its `epic` issue, and point worktrunk at the new branch in
each clone: `wt config state default-branch set release/<version>`.

`workflow_dispatch` with `dry_run` (the default) rehearses everything but the push.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 0126. Parallel work runs in worktrunk worktrees beside the repository, based on the release branch

Date: 2026-10-31
Status: accepted

Context: 2026-10-31, issue #265, at the start of 2.4.0. 2.3.0 closed with eight PRs in flight at once and 2.3.1
with ten, each in a worktree of its own. Those worktrees came from three places, each with a flaw:

- **The desktop app's worktree sessions** go to `.claude/worktrees/<name>` on `claude/<name>`, inside the
repository, kept out of `git add -A` only by a line in `.git/info/exclude`, which no clone carries. On this day
the main checkout had 19 linked worktrees, 17 of them there, several detached and several for merged work.
- **Agents with `isolation: "worktree"`** started at `origin/main`, not at the release branch: on 2026-10-13 three
of four were 49 commits behind `release/2.0.0`.
- **Hand-made worktrees** (`git worktree add`) under the scratchpad and `/tmp`, each named and removed by hand.

The maintainer adopted worktrunk (`wt`, 0.81.0, and its Claude Code plugin at commit `2c04fb370427`) in another
repository the same day, with a handoff of what was measured there. This record fits it to this repository,
whose work lands by a pull request into `release/<version>` with a merge commit
([decision 94](0094-topic-branches-into-a-release-branch-and-fix-trailers.md)), not by a local merge into `main`.

## Decision

**Worktrees go beside the repository**, at `../CodoMetis.TypeKit-worktrees/<branch, sanitized>`, made by
`wt switch -c <type>/<slug>`. The path is set in each machine's worktrunk user config, since the project file
cannot set it. Outside the repository there is no ignore rule to keep right. `.claude/worktrees/` joins the
tracked `.gitignore` for the desktop app's sessions, which ignore the plugin.

**worktrunk's default branch is the open release branch**, `wt config state default-branch set release/<version>`,
set per clone and moved by CONTRIBUTING's "Releasing" when the next release branch is cut. One setting makes
`wt switch -c` branch from the release, the plugin's agent worktrees start there (its hook runs
`wt switch --create`), and `wt remove` and `wt step prune` judge "merged" against the branch topics merge into.

**Nothing lands locally.** The Bash guard refuses `wt merge` and its building blocks `wt step squash`, `rebase`
and `push`, past worktrunk's global options and except with `--help`: by default they squash, rebase and
fast-forward onto the default branch, and none writes a merge commit. The user config turns the four `merge.*`
keys off as a net. The guard reads the words the shell would pass, like its other rules; `wt` called through a
wrapper or a variable passes it.

**Nothing is copied into a new worktree.** No `.worktreeinclude` and no `.config/wt.toml` hook, since a fresh
worktree needs no gitignored file to build, and so no hook waits on `wt config approvals add`.

[design/worktrees.md](../worktrees.md) is the runbook, linked from AGENTS.md and CONTRIBUTING.

## Evidence

Measured 2026-10-31 on macOS (arm64), SDK 10.0.4xx, in a worktree `wt switch -c build/worktrunk-worktrees` made
with no `--base` from `release/2.4.0` at `3be21dc`:

| Step | Result |
|---|---|
| `wt switch -c`, default branch set to `release/2.4.0` | branch and worktree from `release/2.4.0`, at `../CodoMetis.TypeKit-worktrees/build-worktrunk-worktrees` |
| the guard and the tools, cold, as SessionStart builds them | 2 s, no error |
| the whole solution, cold, through `build-verdict build` | 27 s, 33 projects, no error, nothing copied in |
| `CodoMetis.TypeKit.BashGuard.Tests` | 112 passed |

The other repository measured a copy of its build caches at 1.9 GiB in 6.3 s, saving about 2 s, and found the
copied `obj/` holding absolute paths into the primary checkout; the compiler servers and the NuGet cache are
shared per machine, which is why a cold worktree builds fast here too.

The guard's rule was shown to fail against two seeded defects: the detection removed (17 of the new cases fail:
every refused `wt merge` and `wt step` form, and the message test), and the value of `-C`, `--config` and
`--config-set` read as a subcommand (4 fail: the forms after `-C`, after `--config-set`, a step after options, and
the message test).

Not measured: an agent's `isolation: "worktree"` under the plugin starting at the release branch. The plugin
loads only in a session started after its install, and worktrunk's documentation is the source for its hook
running `wt switch --create`.

## Consequences

- A session works in the worktree it was started in. `build-verdict` tests the checkout it was built in, so a
session in the main checkout that edits a worktree judges the wrong tree, and the code graph does not cover a
worktree outside the repository.
- Every clone sets two things the repository cannot: the worktree path in worktrunk's user config and the default
branch in its local git config. A clone that sets neither gets worktrunk's sibling layout (`../repo.branch`)
and branches from `main`.
- An agent worktree made by the plugin is named `agent-<id>`, which PR hygiene refuses as a head branch, and
outlives its agent; the runbook pre-creates worktrees for subagents instead.
- The plugin follows worktrunk's `main`. Its behaviour is re-checked after `claude plugin update`.
1 change: 1 addition & 0 deletions design/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,4 @@ per area ([design](../plan.md)) link to the records they rest on.
| [0123](0123-a-hand-written-operator-is-refused-in-a-query.md) | An arithmetic, bitwise or shift operator a value object declares by hand is refused in an EF Core query, naming the value object and `.Value`, by an interceptor and, for query filters, when the model is built; the generated ones carry `[TranslatedAsWrappedArithmetic]`, which is what tells them apart; a conversion operator is left to EF; refines decision 122 | 2026-10-30 | accepted |
| [0124](0124-an-operator-in-a-query-is-the-wrapped-type-s-arithmetic.md) | In an EF Core query, every generated arithmetic operator is rewritten to the wrapped type's operator over `ProviderValue`, which translates only through TypeKit's converter, its value-object result `Materialize`d and re-typed, never cast; `byte`, `short` and `char` operators are refused; refines decision 122, on decision 123's mark | 2026-10-30 | accepted |
| [0125](0125-a-package-documents-the-third-party-code-it-contains.md) | A package carries a `THIRD-PARTY-NOTICES.txt` only when it contains third-party code (a bundled assembly, copied source, a binary release), built from the shipping closure with the license texts; a package that bundles nothing documents its own license, copyright and dependency list, and its dependencies' licenses are held to an allow-list by a conventions test, not listed (#181); no SBOM is shipped, since GitHub's export is an inventory without license data | 2026-10-30 | open |
| [0126](0126-parallel-work-runs-in-worktrunk-worktrees-beside-the-repository.md) | Parallel work runs in worktrunk worktrees at `../CodoMetis.TypeKit-worktrees/<branch>`, set per machine in worktrunk's user config; worktrunk's default branch is the open `release/<version>`, set per clone, so `wt switch -c`, the plugin's agent worktrees, `wt remove` and `wt step prune` base on it; the Bash guard refuses `wt merge` and `wt step squash`, `rebase` and `push`, since work lands by a PR and a merge commit (decision 94); nothing is copied into a new worktree, which builds the solution cold in 27 s; `.claude/worktrees/` joins `.gitignore`; [design/worktrees.md](../worktrees.md) is the runbook | 2026-10-31 | accepted |
1 change: 1 addition & 0 deletions design/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ you touch; the rest is reference.
| [Mutation survivors, 2026-10-16](mutation-2026-10-16.md) | The first Stryker.NET run, on the Serilog satellite: counts per package, each survivor read as a missing assertion or an equivalent mutant, and the valid mutants the compile lost | one dated page per run |
| [Finder benchmark, 2026-10-16](finder-benchmark-2026-10-16.md) | The 24 release escapes put back as seeds: which test, probe row or nothing detects each, totals per finder as applied, reached and detected, and the accounting every probe round and analyst run reports | one dated page per run |
| [Oracles](../test/oracles/README.md) | What the documents promise as claims held by named tests, every public member claimed or waived, the oracle classes and their seeds, the candidates and the axes a consumer varies ([decision 112](adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md)) | with each feature and each promise a document gains |
| [Parallel work in worktrees](worktrees.md) | The runbook for worktrunk: worktrees beside the repository, based on the release branch, landed by PR, and what to know ([decision 126](adr/0126-parallel-work-runs-in-worktrunk-worktrees-beside-the-repository.md)) | when worktrunk or the plugin changes |
| [The code graph](code-graph.md) | What the codebase-memory graph was measured to find and miss, and how to keep it current; AGENTS.md's "Finding code" rests on it | when the graph is measured again |
| [Probing without the tooling](probing-without-tooling.md) | How BLite applied the probing concept by hand, with no prober, covering array or ledger: what each tool function became, the process as it ran, the issues and PRs it produced, and an adoption guide for trying the same here without the tooling | copied 2026-10-18; as the method is tried here |

Expand Down
105 changes: 105 additions & 0 deletions design/worktrees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Parallel work in worktrees

How several fixes and features run at once, each on its own branch in its own worktree, made and cleaned up
by worktrunk (`wt`). [Decision 126](adr/0126-parallel-work-runs-in-worktrunk-worktrees-beside-the-repository.md)
holds the reasons and the measurements; this page holds the commands.

```
~/RiderProjects/
├── CodoMetis.TypeKit/ ← the main checkout
└── CodoMetis.TypeKit-worktrees/ ← every worktrunk and agent worktree
├── fix-ef-boxed-ordering/ ← branch fix/ef-boxed-ordering (`/` becomes `-`)
└── feat-value-object-arithmetic/ ← branch feat/value-object-arithmetic
```

## Once per machine

```bash
brew install worktrunk # or: cargo install worktrunk
wt config shell install # then start a new shell
claude plugin marketplace add max-sixty/worktrunk
claude plugin install worktrunk@worktrunk # loads in new sessions only
```

Then, in `~/.config/worktrunk/config.toml`, under the identifier `wt config show` prints for this checkout
(`github.com/CaffeinatedCoder/CodoMetis.TypeKit` for a clone of the main repository):

```toml
[projects."github.com/CaffeinatedCoder/CodoMetis.TypeKit"]
worktree-path = "{{ repo_path }}/../{{ repo }}-worktrees/{{ branch | sanitize }}"
# A net under the guard's refusal of `wt merge`: nothing lands locally here.
merge.squash = false
merge.rebase = false
merge.ff = false
merge.commit = false
```

The project file `.config/wt.toml` cannot set the path, so each machine sets it. A clone without a remote is
keyed by its path, and adding a remote later changes the key: run `wt config show` again then.

## Once per release branch

```bash
wt config state default-branch set release/2.4.0
```

worktrunk's default branch is what `wt switch -c` branches from, what the plugin's agent worktrees start at,
and what `wt remove` and `wt step prune` call "merged". Left at `main`, a new branch misses the open release's
work and a merged topic never counts as merged. The setting lives in the clone's local git config, so every
clone sets it, and CONTRIBUTING's "Releasing" moves it with the next release branch.

## Every task

```bash
git fetch origin release/2.4.0:release/2.4.0 # the base, current
wt switch -c fix/ef-boxed-ordering --no-cd # branch from release/2.4.0, worktree beside the repo
```

Start the session *in* `../CodoMetis.TypeKit-worktrees/fix-ef-boxed-ordering`. Its SessionStart hook builds the
guard and the tools there ([decision 105](adr/0105-every-tool-is-built-in-the-session-s-checkout.md)), and
`build-verdict` tests the checkout it was built in, so a session started in the main checkout judges the wrong
tree. Name the branch from the issue's type label (`feat/`, `fix/`, `docs/`, `test/`, `build/`), as PR hygiene
reads it.

To hand subagents a worktree each from one session, make the worktrees with `wt switch -c ... --no-cd` and name
the path in each agent's prompt, without `isolation: "worktree"`.

```bash
wt list # every worktree, ahead/behind release, agent markers
git push -u origin fix/ef-boxed-ordering # then open the PR into release/2.4.0
```

Work lands by the PR and its merge commit
([decision 94](adr/0094-topic-branches-into-a-release-branch-and-fix-trailers.md)). The Bash guard refuses
`wt merge` and `wt step squash`, `rebase` and `push`, which squash, rebase or fast-forward locally by default.

After the merge:

```bash
git fetch origin release/2.4.0:release/2.4.0 # wt judges "merged" against the local branch
wt remove fix/ef-boxed-ordering # deletes the branch once merged; -D forces
wt step prune --dry-run # sweeps merged worktrees older than a day
```

## What to know

- **Nothing is copied into a new worktree.** A fresh one builds the solution cold in 27 s and its tools in 2 s
(measured 2026-10-31): no gitignored file is needed, so there is no `.worktreeinclude` and no `post-start`
hook. Add both if that changes, and measure first: copied `obj/` folders keep absolute paths into the main
checkout.
- **Agents with `isolation: "worktree"` follow the plugin**: its hook makes their worktrees with
`wt switch --create`, so they land beside the repository and start at the default branch (worktrunk's
documentation; not yet observed here, where agents used to start at `origin/main`). They are named `agent-<id>`
on a branch of the same name, which PR hygiene refuses, and outlive the agent until `wt remove` or
`wt step prune`. Pre-creating the worktree avoids both.
- **The desktop app's own worktree sessions ignore the plugin.** They go to `.claude/worktrees/<name>` on
`claude/<name>`, which `.gitignore` keeps out of `git add -A`.
- **Ids minted in parallel collide**, textually clean: decision records, `design/knowledge/` entries and claim
ids in `test/oracles/claims.json`. Hand each parallel task its numbers, and check them against the release
branch right before the merge.
- **The code graph does not cover a worktree outside the repository.** Index it under a name of its own, as
AGENTS.md's "Finding code" says, or search the source.
- **The plugin installs from worktrunk's `main`, not a release.** Its worktree hooks need `wt` on Claude Code's
`PATH` (`/opt/homebrew/bin` on macOS). Check `claude plugin list` after `claude plugin update`.
- **The guard reads words, not every shell.** `wt` called through a wrapper (`cargo run --bin wt`, a variable)
passes it.
Loading
Loading