AirFS¶
One directory. Many repositories. No copies. ๐ช
airfs is a Go SDK โ and a small CLI โ that layers AI resource directories
(agents, skills, commands, scripts) from several repositories into a single
read-only merged view, served as a FUSE mount written in pure Go.
๐งฉ The problem¶
Your agent tooling wants every skill under one directory:
But skills are authored in the repository that owns them โ one per team, per product, per concern. So you have to bridge the gap, and the usual bridges rot:
What doesn't work
- ๐ Copying โ the copy drifts from the original the moment someone edits it.
- ๐ Hand-made symlinks โ they drift from the source list instead.
- ๐งฑ
mergerfsโ a system binary your distribution only packages with root.
โจ The idea¶
Don't move anything. Merge the directories in place and expose the result as a view. Each resource keeps living in โ and is edited in โ its own repository.
flowchart LR
A["๐ฆ ai-resources<br/>skills/"]:::src
B["๐ฆ ai-tools<br/>skills/"]:::src
C["๐ฆ ai-maintainer<br/>skills/"]:::src
M{{"๐ช airfs<br/>read-only merge"}}:::mid
V["๐ ~/.ai-resources/skills/<br/><i>merged view</i>"]:::out
A --> M
B --> M
C --> M
M --> V
classDef src fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
classDef mid fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#e65100
classDef out fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1b5e20
Edit a file in its repository โ the change is visible through the view immediately. No sync step, no copy to refresh. ๐
๐๏ธ The vocabulary¶
Five words carry the whole model.
-
๐ฆ Source
One contributing directory tree โ normally a git working copy. Sources are an ordered list, and that order is the precedence order.
-
๐ท๏ธ Kind
A category of resource = one subdirectory name:
agents,skills,commands,scripts. The set is fixed and built in, so unrelated top-level directories can never leak into the view. -
๐ Entry
One resource inside a kind, named directly under it โ a directory like a skill, or a single file like a command. The entry is what collides and what gets shadowed, and it is shadowed whole.
-
๐ฅ Stratum
One source's contribution to one kind โ
<source>/<kind>/. A kind's view is the ordered stack of its strata. -
๐ฏ Target
The resource folder the view lives under โ
~/.ai-resourcesby default. It holds the configuration file and one mounted subdirectory per kind.
๐ฅ Precedence: latest source wins¶
Sources are declared from the most general to the most specific โ global,
then organisation, then project. When two repositories both ship a skill named
commit, the one declared last wins, and it wins whole โ no half-merged
resource assembled from two places.
flowchart TB
subgraph S["Declared order (last wins)"]
direction LR
S1["1๏ธโฃ global<br/>commit โ shadowed<br/>review โ
"]:::mix
S2["2๏ธโฃ organization<br/>deploy โ
"]:::win
S3["3๏ธโฃ project<br/>commit โ
<br/>audit โ
"]:::win
end
S --> R["๐ Merged view<br/>commit ยท review ยท deploy ยท audit"]:::out
classDef win fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
classDef mix fill:#fff8e1,stroke:#f9a825,color:#f57f17
classDef out fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
Precedence is independent per kind: a source can win commit under skills
while losing commit under commands.
Shadowing is always reported, never silent
airfs sources lists every shadowed entry with its winner and its losers.
A silent shadow is the failure mode that makes a merged view untrustworthy โ
you edit a file, nothing happens, and nothing tells you why. ๐ต๏ธ
The view is read-only
Writes are rejected, not routed to a source โ and the kernel enforces it, so no process can write through the mount even by mistake. A new file in the merged view belongs to some repository and the view has no basis to pick one; a write that succeeded would bypass that repository's git history, review, and tests. Edit in the source repo. โ๏ธ
๐ช How the view is exposed¶
One FUSE mount per kind, all served by one process:
~/.ai-resources/
sources.txt # the ordered layers โ never masked by a mount
agents/ # mountpoint
skills/ # mountpoint
commands/ # mountpoint
scripts/ # mountpoint
airfs mount establishes them together and blocks while serving; --detach runs
it as a daemon instead. airfs umount releases them โ including a stale
mountpoint left behind by a serving process that died.
Mounting needs /dev/fuse and a setuid fusermount3, both of which ship with
your distribution's FUSE package. Not sure you have them? Run airfs doctor โ
it checks the host and names the package to install. ๐ฉบ
โ๏ธ Configuring it¶
One plain-text file at the root of the target, one path per line. The most common diff is a single added line.
# ~/.ai-resources/sources.txt โ order is precedence, last wins
~/sylvan/ai-resources # 1st โ global
~/sylvan/ai-tools # 2nd
$WORK/ai-maintainer # 3rd โ wins every collision
Comments, ~, and $VAR are expanded; relative paths resolve against the
config file's own directory, never the working directory. An unset variable or a
missing source directory is an error, not a shrug โ a view quietly missing a
repository is the harder failure to diagnose. ๐ง
๐ ๏ธ The commands¶
| Command | Does |
|---|---|
sources ๐ |
Resolve the config and report it: order, counts per kind, shadowing. Touches nothing. |
mount ๐งต |
Serve the merged view under the target, one mount per kind. --detach to daemonise. |
umount ๐งน |
Release the mounts, including stale ones. |
status ๐ |
Whether the target is served, live or stale โ and what's visible through it. |
doctor ๐ฉบ |
Check /dev/fuse and fusermount3, and say what to install. |
๐ง Status¶
Implemented
Every spec in docs/specs/ is implemented: the library
and the airfs command exist, and the merge, the mount, and the CLI are
covered by tests. airfs replaces a mergerfs-based Makefile setup in
ai-resources.
No implementation change lands without an agreed spec.
๐ Where next¶
- ๐ Get started โ install and first mount, in five minutes
- ๐ User guide โ layers, precedence, mounting, the Go API
- ๐ Specs โ the model, the merge, the mount, the CLI
- ๐ค Contribute โ setup, targets, and quality gates