bloom
CLAUDE.md46 starsChanged 48 days agoMITView on GitHub
Project skills for development builds, releases and Swift work are indexed in AGENTS.md. Claude and Codex share the same skill files; load only the skill and references relevant to the task. A macOS 26 app for running coding agents in git worktrees. One window: a sidebar of projects and their workspaces, a transcript in the centre, a terminal, an inspector. A workspace is a real worktree on disk, which is why so much of what follows is about not destroying…
- Installs packages
In this repository6 other files it gives its agents.
- AGENTS.mdAGENTS.md
- bloom-dev-buildSkill
- bloom-releaseSkill
- swift-concurrency-proSkill
- swift-testing-proSkill
- swiftui-proSkill
The file
We split this CLAUDE.md into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 329
No em dashes and no en dashes anywhere: Use a comma, a full stop or brackets.
- Distinctive: line 351
If a piece of work cannot be verified without one of these, say so and stop.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Bloom
Project skills for development builds, releases and Swift work are indexed in AGENTS.md. Claude and Codex share the same skill files; load only the skill and references relevant to the task.
A macOS 26 app for running coding agents in git worktrees. One window: a sidebar of projects and their workspaces, a transcript in the centre, a terminal, an inspector. A workspace is a real worktree on disk, which is why so much of what follows is about not destroying one.
Longer documents, pointed at rather than repeated here: README.md for what the app is,
RELEASING.md for signing, notarising and the appcast, docs/CODEX.md for the Codex app-server
protocol as measured, docs/GROK.md for Grok's ACP over stdio, docs/GITLAB.md for GitLab through glab, docs/PROTOCOL.md for Claude Code's stream-json,
docs/AGENTS-INTEGRATION.md for how agent CLIs are detected, docs/BRIDGE.md for the MCP
bridge an agent calls back in through and which callers may call what, docs/PLAN.md for what was
built and in what order, docs/start-from.html for the design note the create sheet's source picker
was drawn from, which is a page to open in a browser rather than to read here.
Target boundaries
Sources/BloomCore holds shared non-UI behaviour: Store, Git, Shell, WorkspaceManager,
the agent protocols, the parsers, the models. It never imports a UI framework.
Sources/Bloom is the SwiftUI app and the only target allowed to import SwiftUI, AppKit, SwiftTerm
or Sparkle.
Sources/bloom-bridge is the MCP stdio shim an agent CLI launches as a child process, which
relays lines to the running app over a unix socket. Three lines of main.swift; everything worth
testing is BridgeShim in the core. It is a relay with nothing to draw, so it is held to the same
line as the core.
Sources/bloom-sleep-helper is a separate privileged helper for Keep Awake. It does not
import a UI framework. BloomCoreTests exercises the core module.
Sources/bloom-mail-server supervises the bundled test inbox. Its stdin pipe is held by Bloom;
EOF stops the server after a clean quit or crash. Tools/mailpit-binary.sh verifies the pinned
release archive before Tools/build.sh puts the binary and licence into the signed app bundle.
make lint holds that line for both, and it looks for the framework rather than for a literal,
because import Cocoa re-exports the whole of AppKit and import class AppKit.NSView names
AppKit without containing the words next to each other. SwiftTerm and Sparkle need no rule: only
the app target declares them in Package.swift, so importing either anywhere else does not link.
Tests/BloomCoreTests depends on BloomCore alone. Read Package.swift: the test target has one
dependency and it is not the app. So a decision taken inside a view is a decision nothing can
test. When behaviour needs a test, and most does, it belongs in BloomCore as a pure function or a
type, with the view calling it. That is the whole reason the split exists.
Build and test
Everything real is a script in Tools/; the Makefile is the index.
make list the targets make lint Tools/house-rules.sh
make build compile every target make test the BloomCore suite
make swiftlint Tools/swiftlint.sh
make app assemble a debug .app make run release .app, launched
make master install /Applications/Bloom.app (see the guard below)
make dev-fast install current edits as Bloom Dev (debug)
make dev install committed HEAD as Bloom Dev (release)
make dev-db copy the real database into the dev copy
make release sign, notarise and staple a zip and a disk image into dist/
make dmg wrap the newest built .app in the beach disk imageAnything that takes an argument is run directly: ./Tools/test-core.sh DiffParser,
./Tools/master.sh v0.3.0, ./Tools/dev-build.sh --no-launch.
./Tools/test-core.sh mirrors the core sources into a throwaway package with no app target, so one
broken view cannot stop the core suite. Its head documents the environment it reads: BLOOM_TEST_ID
for a stable work and build directory, BLOOM_TEST_RUNS to run the suite repeatedly and shake out
flakes, BLOOM_LOCAL_AGENTS=1, BLOOM_LOCAL_SETTINGS=1 and BLOOM_LOCAL_SKILLS=1 to assert
against this machine, with BLOOM_LOCAL_PROJECT naming the checkout the last of those reads,
BLOOM_LIVE=1 to drive the real claude binary (this costs money), and
BLOOM_TEST_SWIFT_ARGS for flags like --sanitize=thread.
A mutating call cannot go inside #expect. The macro rewrites its argument into a closure
taking the value immutably, so #expect(flow.advance()) fails to compile with "cannot use mutating
member on immutable value: $0 is immutable", and the error is reported against expanded code
rather than against the line you wrote. Lift it: let moved = flow.advance() and then
#expect(moved). This has cost two separate agents an hour each, on TranscriptRowHeights and on
OnboardingFlow, and it is the sort of thing nobody deduces twice.
Never assert a wall clock duration, and never sleep for something another task must do first.
Assert what happened: the call returned, it returned that error, the counter moved, the subscriber
was registered before the event was sent. Where a bound is genuinely needed, make it a ceiling of
thirty or sixty seconds and say in a comment that it is a cap on infinity rather than a
measurement, because what it guards against is not "slow", it is "never". The macOS runner shares a
machine with six and a half thousand tests and the same suite has measured between 47 and 65
seconds on one commit, so a number taken from a run on your own Mac is a description of that Mac.
The test before writing one is: what would this number have to be wrong by for the code to be
broken? If the answer is "any amount, it never returns at all", the number is a ceiling and should
be large. And a ceiling has to sit below whatever kills the test from outside, which is usually
a suite's own .timeLimit: a bound at or above that limit can never report its own failure,
because the harness's stopwatch fires first and the assertion under it never runs. If there is no
answer to the question at all, something else should be asserted instead. This was written after
producing the fourth instance in one night, an hour after fixing the third, which is how a careful
person gets it wrong.
A green make test does not mean the app compiles. The mirror has no app target, so the
core suite has stayed green while Sources/Bloom was broken, four times, every one of them a
widened enum leaving a switch in a view non-exhaustive. Run make build before committing
anything that adds a case to an enum.
And a green swift build here is not the build that gates a release. .github/workflows/test.yml
compiles the app target with -warnings-as-errors, which Package.swift deliberately does not
carry, so a warning that is a line of yellow on this Mac is a release that will not build, found after the merge by whoever comes next. Run swift build -Xswiftc -warnings-as-errors before pushing; the last one
to catch somebody was a var that is never mutated, which the local build says nothing about.
Zero warnings, and make lint green, before anything is committed.
make lint and make swiftlint are two different linters and both have to pass.
Tools/house-rules.sh is the conventions no off the shelf tool knows: no em dashes, British
spelling, typed ids, a view that does not run a subprocess, only the app target importing a UI
framework. SwiftLint is the half every Swift codebase shares, and .swiftlint.yml is tuned rather
than default, because the defaults reported 1,634 violations here. The head of that file carries
the count each disabled rule produced and the reason it is off, so a rule is argued with rather
than guessed at. Both run in the lint job of .github/workflows/test.yml, on the Linux runner.
SwiftLint is not installed by anything here. brew install swiftlint, or take the binary from the
releases page; Tools/swiftlint.sh says so and stops rather than installing it for you. CI pins
its own copy. ./Tools/swiftlint.sh --fix applies what SwiftLint can correct itself, and the
diff has to be read: it once rewrote let _ = inside a @ViewBuilder into code that does not
compile.
A rule of ours goes in Tools/house-rules.sh, never in a custom_rules: block. SwiftLint
matches a custom rule through SourceKit, there is no SourceKit on Linux, and the lint job prints
"Skipping enabled rule 'custom_rules'" and goes green. A rule that passes on the author's Mac and
is not run by the thing that gates the merge is worse than no rule, so .swiftlint.yml has no
such block and says why.
Why there is no Xcode project
Because there is nothing left for one to do, and this was measured rather than assumed. The question keeps coming back, so the answer is written down here.
Xcode already has the targets and the schemes. Open Package.swift in Xcode and it builds,
runs, debugs, profiles and previews. xcodebuild -list in this directory, with no .xcodeproj
anywhere, discovers the package products and schemes.
Apple's own position is in the tooling: swift package generate-xcodeproj was removed years ago,
and swift build --build-system xcode is labelled "discouraged" in its own help text.
The one thing a project file really does buy is the recommended-settings prompt, and here it
would land on a build nobody ships. That prompt is a PBXProject mechanism and nothing else:
the strings live in DevToolsCore.framework next to PBXProjectAttribute_LastUpgradeCheck,
PBXProject setLastUpgradeCheck: and _runUpgradeChecksIfNecessary, and what it compares is the
LastUpgradeCheck stamp in project.pbxproj against the Xcode you opened it with. A package has
no project.pbxproj, so there is nothing to stamp, which is why the prompt never appears. It is
a real feature and it is genuinely unavailable here; the question is what it would modernise. It
edits XCBuildConfiguration, which is a set of build settings that swift build never reads.
make build, Tools/build.sh, Tools/test-core.sh, .github/workflows/test.yml and
release.yml all go through SwiftPM, so a project accepting a recommended setting and the app
everybody actually installs would be two different builds, free to disagree, with only the project
file being told about it.
And a checked-in project file rots here faster than most. This repository took 728 commits, 21
merges and 964 new Swift files in its first eight days, with several agents in parallel worktrees
merging into main. Xcode 26 does remove the oldest objection, because a
PBXFileSystemSynchronizedRootGroup picks files up from a folder without listing them, so the
"every new file has to be added by hand" complaint is out of date and should not be repeated. What
does not go away is that the file would be a second description of a build that only one person
opens, next to a Package.swift that everything else reads.
Generating it instead does not rescue the argument, it kills it. XcodeGen or Tuist from a
spec, or a Tools/xcodeproj.sh that writes one on demand, would avoid the merge conflicts, but the
whole benefit above is a stamp Xcode writes back into project.pbxproj and compares next time.
Regenerate the project and that stamp goes with it, so the modernisation prompt you wanted either
never fires or fires every time. A generated project is the right answer to a different question.
What to do instead, when the toolchain moves. The package equivalent of accepting recommended
settings is swift-tools-version and the language mode at the head of Package.swift, which are
6.2 and .swiftLanguageMode(.v6) today, plus .enableUpcomingFeature for anything the next
release wants opted into. CI already refuses to build on an Xcode older than 26 and compiles with
-warnings-as-errors, so a toolchain that changes its mind about something says so on the next
push rather than in a dialog nobody opened.
.gitignore has ignored *.xcodeproj since before this was written. Leave it there. If you want
Xcode, open Package.swift.
Where a green answer comes from
Do not run tests on this Mac unless the owner asks you to. Rely on CI by default. That means
no make test, no swift test and no ./Tools/test-core.sh, not even with a filter. Build with
swift build -Xswiftc -warnings-as-errors, run make lint and make swiftlint, push, and let CI
run the tests, which for the core suite means a release or a dispatched run. The owner is working at this Mac while you run and several agents build at once: a
day of local test runs took the load average to 205, and running Swift processes get killed to win
the machine back, so a local run is both a stall for him and an answer you may never get. When the
owner does ask for a local run, run only the filter that covers what you touched, for example
./Tools/test-core.sh DiffParser, never the full suite.
A pull request runs lint and the secrets scan. .github/workflows/test.yml runs the house
rules, SwiftLint and secrets scan on every pull request push. The macOS
pair, the app target with -warnings-as-errors and the whole core suite, does not run there,
because it was nearly all of the Actions bill and pull requests here are merged without waiting
for it. It runs in front of every Mac release (release.yml calls it and
will not build if it fails), and by hand with gh workflow run test.yml --ref <branch>. So
swift build -Xswiftc -warnings-as-errors on this Mac is the only compile a change gets before it
merges, and it is not optional. When a change needs the suite run before it lands, dispatch the
workflow on its branch and read the result; gh run watch and gh run list --branch <name> are how
you wait without polling the browser.
A branch pushed with no pull request open is not built at all, and merging does not repeat the run
on main.
.github/workflows/slow-checks.yml is separate, gates nothing and runs only by hand: Thread
Sanitizer, coverage and dead code, too slow to put in front of a push. Dispatch it when you want
those questions answered, and do not wait on it.
Persistence goes through Store
One actor, one SQLite file, and one rule: upsert creates a row, update modifies one.
upsert writes every column from the value it is handed, so it is correct only when that value was
built there and then. Hand it a row read a few seconds ago and it carries every column back to what
it looked like then. update(workspaceID:) and its siblings read the row inside the actor, apply
the change and write, with no suspension in between.
This is not tidiness. A workspace row has a diff stat refresh writing every six seconds, an archive
that takes seconds of disk work, a panel somebody sits in for a minute, and an agent turn that runs
for ten. Whole-value writes rolled each other back, and the worst of it was a row that said a
workspace was live after its worktree had been deleted. Tests/BloomCoreTests/WorkspaceWriteIsolationTests.swift
is that bug written down: a write changes the columns it names and no others. Add a column and
update picks it up; reach for upsert on an existing row and the bug is back.
Views
A type conforming to View does not run a subprocess. Shell and Git are reached from a store,
a model or a helper type beside the view (FileRevert, FileIndex, TerminalPersistence, or
WorkspaceModel over in State/), never from a body or a button action. make lint holds this
one too, by looking for await Git. and await Shell. under Sources/Bloom/Views/: the subprocess
calls are all async and the pure helpers on the same types are not, so the test costs no exception
list of its own. The three of those that live under Views/ are named in the allow-list because
they are what a view calls instead of reaching for a process itself; WorkspaceModel is outside
that path, so the rule never sees it and needs no entry. CreateWorkspaceView was the last
exception, calling Git.branches from its own .task; that loading and both branch decisions it
fed are WorkspaceStartContext in the core now, tested, so its entry came off and the allow-list
in Tools/house-rules.sh is back to the three helper types it was meant to hold.
Where a file goes
Sources/BloomCore is grouped by subject, one directory deep and no deeper:
Agent/ running one, its events, its quotas, its turns; Codex/ is its own protocol
Bridge/ the unix socket, the MCP tools an agent calls back in with
Git/ git itself, and everything read out of a diff
Forge/ shared request, check and merge policy, forge routing
GitHub/ gh commands, GitHub payloads and authentication
GitLab/ glab commands, GitLab payloads and authentication
Workspace/ creating, starting, archiving, restoring, and a project's own settings
Mail/ Mailpit: its API, the app-wide mailbox, and connection state
Transcript/ what a turn is made of: rows, tools, subagents, attachments, prompts
Persistence/ Store, SQLite, the settings file, the old app's leftovers
Model/ the row types, the typed ids, and the lifecycle rules over them
Presentation/ decisions that exist for the window and hold no UI framework
Ocean/ the chart, which is its own small world
System/ this Mac: the shell, notifications, updates, other applications
Support/ small things with no subject of their ownIt was 274 files in one directory, which is not a structure, it is a list. The grouping is by
what a file is about, never by what it is: no Extensions/, no Helpers/, no Protocols/,
because those tell a reader nothing they could not see from the file itself.
Presentation/ is the one that needs a sentence. It holds decisions the window needs and that
have no import SwiftUI in them: which tab is next, what the sidebar's summary line says, whether
two colours are far enough apart. They are here rather than beside the views for the reason the
whole three-target split exists, and they are in their own directory rather than mixed into the
subjects so that "this is a view's decision, moved" stays visible.
Sources/Bloom is grouped the same way, by pane rather than by kind. Views/ holds one
directory per region of the window (Sidebar, Center, Inspector, Home, Transcript,
Terminal, Chrome, Tabs) and one per thing that gets a window or a sheet of its own
(Archive, Code, CreateWorkspace, Markdown, Oceans, OpenIn, RepoSettings), and the
two that outgrew a single directory are split by what they are for rather than by what they are:
Center/Composer, Center/Panes, Center/Attachments, Center/Browser; Chrome/Window,
Chrome/Settings, Chrome/MenuBar, Chrome/App, Chrome/Feedback, Chrome/Notices.
Incoming mails is an app-wide destination opened from the sidebar footer. Its list and reader
share one connection to Mailpit, so they live together rather than under workspace panes. By
default Bloom starts the bundled server on loopback when the feature is enabled. Dev uses separate
ports and application data. The SMTP port can be changed in Settings; the API port stays internal.
Four directories sit beside Views/ and are not panes, because none of them is drawn in one
place. Design/ is the theme and the snapshot galleries, State/ is AppModel with its
extensions, the models under it and Mailbox, the app's one connection to Mailpit, Intents/ is
App Intents, Shortcuts and the Services menu, and System/ is the app target's half of talking to
this Mac.
A directory with one file in it is not a subject. Leave it at the folder's root and give it one
when a second arrives. Tools/house-rules.sh names about twenty of these paths in its allow-lists,
so a move has to update them: make lint says so immediately, which is the point.
One subject per file
AppModel reached 2,421 lines: four enums and structs stacked above the class, fourteen MARK
sections inside it, and not one of those was a decision anybody took. Each arrived as three methods
put beside the last three, because the file was already open.
A type gets a file. A subject gets a file. A type that has grown a second subject is split with
an extension in the same directory, named Type+Subject.swift. AppModel+ProjectIcons.swift and
AppModel+TranscriptSearch.swift are the shape, each opening with a paragraph saying what the file
is for; AppModel+Naming.swift is the same split without that paragraph, and is the poorer for it.
What stays in Type.swift is what nothing else can hold: the stored properties, because Swift has
none in an extension, the initialiser, and the members that genuinely coordinate between subjects.
Two things decide where the cut goes, and neither is taste. A stored property cannot move, so a
grouping that needs one moved is the wrong grouping. And private is file scoped, so every member
that leaves either takes the private state it touches with it or forces that state wider. Count the
widenings before splitting. Five files that keep a type's internals are worth more than nine that
publish them, and a cut that widens the properties whose doc comments say they have one writer is a
cut in the wrong place. Store is the case that settles it: 2,603 lines around one private let
db, where every extension would have to publish the connection, so it stays as it is.
A long file is not the failure, and make lint has no rule here because every threshold tried
fired on files that were right. A 1,200 line ceiling needs an exception list the day it is written,
and half of what goes on that list is correct as it stands. Counting top-level types fires on
CodexEvent.swift at twenty-two, Models.swift at fourteen and eighty other files besides, every
one of them fine. Store at 2,603 lines is right and AppModel at 2,421 was not, and no number
tells them apart. The failure is a file nobody can find anything in. The question before adding to
one is whether the thing being added is what the file is about, and the answer to no is a new file
rather than a longer one.
Comments
Comments say why, never what, and the good ones name the bug that forced the design. Read the
head of Tools/build.sh, Store, or WindowChrome for the register: a paragraph that explains
what was tried, what broke, and what the measurement was, which in WindowChrome is the hex value
the title bar came out at. A comment restating the line under it is noise; the note in BloomApp
saying "not .hiddenTitleBar, because that left the traffic lights floating" survives the next
person who thinks they have a tidier idea.
Prose
No em dashes and no en dashes anywhere. Use a comma, a full stop or brackets. British
spelling. The app is called Bloom, and it had another name before it was renamed, which still
arrives from stale memory. make lint checks all three, knows the old name so this file does not
have to, and names the file and line.
Do not take over the machine
The owner is sitting at this Mac, working, while you run. Anything that takes focus, moves the pointer, opens or closes a window, launches or quits an application, or puts a window in front of what he is reading is an interruption to a person, not a step in a task. Ask before doing any of it, except for restarting Bloom Dev as described below.
Needs permission every time: launching an app in a way that activates it (open -g does not, and
is the one to reach for), NSApp.activate or anything that makes a process frontmost, quitting or
signalling an application other than Bloom Dev, sending a keystroke or a click or any synthetic
event, capturing or recording the display, changing a system setting including the appearance,
and opening a browser or an editor.
Needs no permission: offscreen rendering, anything headless, and capturing your own window by
its window id after opening it with open -g. Agents may quit and relaunch Bloom Dev as needed for
development. Relaunch it with open -g so it does not take focus.
If a piece of work cannot be verified without one of these, say so and stop. An honest "I could not photograph this" is a better outcome than an interruption, and it is what several agents have correctly chosen. A capture that films the display shows whatever is in front of your window, which has twice turned out to be the owner's own screen.
The dev build, and the rules that keep the owner's data alive
Bloom is developed in Bloom. The app you are running inside is the owner's, holding his real projects, and he is using it right now.
Never touch any of these. /Applications/Bloom.app. ~/Library/Application Support/Bloom/.
The be.spatie.bloom UserDefaults domain. Not to test something, not briefly.
Use make dev-fast for everyday local development. It builds current files in debug mode,
including uncommitted edits and untracked files Git does not ignore, with a persistent cache per
checkout. Use make dev for committed HEAD in release mode, or ./Tools/dev-build.sh <ref> for a
specific committed revision. Both install the same ~/Applications/Bloom Dev.app, keep its
existing dev data, and restart only the dev copy. Fast mode cannot be combined with a revision.
For agent verification without installation or launch, use
./Tools/dev-build.sh --fast --no-install. To install without restarting, use
./Tools/dev-build.sh --fast --no-launch.
See .claude/skills/bloom-dev-build/SKILL.md for the full workflow.
Bloom Dev has its own identity: bundle id be.spatie.bloom.dev, and with it its own
preferences domain, its own saved window
state and its own notifications; its own database under ~/Library/Application Support/Bloom Dev/
through BLOOM_DB_PATH in LSEnvironment, and therefore its own tmux socket; its own bloomdev:
URL scheme, so it cannot swallow a bloom:// link meant for the real copy. It is told apart by a
rust and orange icon, by "Bloom Dev" in the Dock and the switcher, and by [DEV] in front of every
window title. Both copies run at once.
Open the dev copy with open, never by running its executable. LSEnvironment is applied by
LaunchServices, so ~/Applications/Bloom\ Dev.app/Contents/MacOS/Bloom started by hand gets no
BLOOM_DB_PATH. That used to mean it fell back to the real database and defeated every separation
above. It does not any more: with no BLOOM_DB_PATH, Store derives the directory from
Bundle.main.bundleIdentifier, and only be.spatie.bloom resolves to Bloom. The dev bundle id
resolves to Bloom Dev, which is where make dev points it anyway; a binary in no bundle at all,
which is swift run or .build/debug/Bloom and which nothing used to warn about, resolves to
Bloom (unbundled) and starts empty. Open it properly all the same, because the check at the foot
of dev-build.sh is worth having and because two agreeing mechanisms are the point.
make dev-db copies the real database into the dev container so there is something real to look
at. It never writes back. It copies the -wal and -shm as well as bloom.sqlite, because in WAL
mode everything since the last checkpoint lives in the WAL and the main file alone is stale, and it
never opens the real database at all, because opening a WAL database writes to it. It also points
the copied workspace rows at a root that does not exist, so the dev copy can show every workspace
and cannot delete a real worktree. --keep-paths opts out and says why you should not.
make master will refuse if you are inside the app it would replace, because that script
removes /Applications/Bloom.app and kills the process running from it. Tools/guard.sh finds the
app either as a real ancestor of this shell or, for a terminal pane whose tmux server has reparented
away, by the socket name derived from the database path. Do not work around it. Use the isolated
dev build described above instead.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

