loomwork 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ source = "github.com/ProperlyDo/agents-rules"
2
+ rules = ["python"]
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: Release
3
+
4
+ on:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ # two quick pushes must not release the same version twice
10
+ concurrency: release
11
+
12
+ jobs:
13
+ check:
14
+ name: Check for a new version
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ outputs:
19
+ version: ${{ steps.version.outputs.version }}
20
+ unreleased: ${{ steps.version.outputs.unreleased }}
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ with:
24
+ persist-credentials: false
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v10.3.0
28
+
29
+ - name: Compare the version with PyPI
30
+ id: version
31
+ run: |
32
+ version="$(uv version --short)"
33
+ echo "version=$version" >> "$GITHUB_OUTPUT"
34
+ if curl --silent --fail --output /dev/null "https://pypi.org/pypi/loomwork/$version/json"; then
35
+ echo "unreleased=false" >> "$GITHUB_OUTPUT"
36
+ else
37
+ echo "unreleased=true" >> "$GITHUB_OUTPUT"
38
+ fi
39
+
40
+ build:
41
+ name: Test and build
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ contents: read
45
+ needs:
46
+ - check
47
+ if: needs.check.outputs.unreleased == 'true'
48
+ steps:
49
+ - uses: actions/checkout@v7
50
+ with:
51
+ persist-credentials: false
52
+
53
+ - name: Install uv
54
+ uses: astral-sh/setup-uv@v10.3.0
55
+
56
+ - name: Test
57
+ run: uv run --locked python -m unittest discover
58
+
59
+ - name: Build
60
+ run: uv build
61
+
62
+ - name: Store the distribution packages
63
+ uses: actions/upload-artifact@v7
64
+ with:
65
+ name: python-package-distributions
66
+ path: dist/
67
+
68
+ publish:
69
+ name: Publish
70
+ runs-on: ubuntu-latest
71
+ # trusted publishing: PyPI trusts this workflow and environment, so no token is stored
72
+ environment: pypi
73
+ permissions:
74
+ contents: write
75
+ id-token: write
76
+ needs:
77
+ - check
78
+ - build
79
+ env:
80
+ TAG: v${{ needs.check.outputs.version }}
81
+ steps:
82
+ - uses: actions/checkout@v7
83
+
84
+ - name: Install uv
85
+ uses: astral-sh/setup-uv@v10.3.0
86
+
87
+ - name: Download the distribution packages
88
+ uses: actions/download-artifact@v8
89
+ with:
90
+ name: python-package-distributions
91
+ path: dist/
92
+
93
+ # files already on PyPI are skipped, so re-running after a later step failed gets past here
94
+ - name: Publish on PyPI
95
+ run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/
96
+
97
+ - name: Tag the release
98
+ run: |
99
+ if ! git ls-remote --exit-code --tags origin "refs/tags/$TAG" > /dev/null; then
100
+ git tag "$TAG"
101
+ git push origin "$TAG"
102
+ fi
103
+
104
+ - name: Create the GitHub release
105
+ env:
106
+ GITHUB_TOKEN: ${{ github.token }}
107
+ run: |
108
+ if ! gh release view "$TAG" > /dev/null 2>&1; then
109
+ gh release create "$TAG" dist/* --title "$TAG" --generate-notes
110
+ fi
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ .idea/
3
+ __pycache__/
4
+
5
+ # loom writes these from .agents.toml, per machine — only the declaration is committed
6
+ .agents/
7
+ .claude/
@@ -0,0 +1,40 @@
1
+ # Ruff's version is `rev` below and nowhere else — the lint job runs these same hooks
2
+ # rather than installing its own ruff.
3
+ repos:
4
+ - repo: https://github.com/pre-commit/pre-commit-hooks
5
+ rev: v6.0.0
6
+ hooks:
7
+ - id: check-added-large-files
8
+ exclude: ^uv\.lock$ # a resolved dependency tree is legitimately large
9
+ - id: check-case-conflict
10
+ - id: check-merge-conflict
11
+ - id: check-toml
12
+ - id: check-yaml
13
+ - id: check-json
14
+ - id: debug-statements
15
+ - id: end-of-file-fixer
16
+ - id: mixed-line-ending
17
+ - id: trailing-whitespace
18
+ exclude_types: [markdown]
19
+
20
+ - repo: https://github.com/astral-sh/ruff-pre-commit
21
+ rev: v0.16.8
22
+ hooks:
23
+ - id: ruff-check
24
+ args: [--fix]
25
+ - id: ruff-format
26
+
27
+ - repo: https://github.com/astral-sh/uv-pre-commit
28
+ rev: 0.12.9
29
+ hooks:
30
+ - id: uv-lock
31
+
32
+ - repo: local
33
+ hooks:
34
+ # this package deletes files in the user's home; the suite is what says it deletes the right ones
35
+ - id: tests
36
+ name: unittest
37
+ entry: uv run python -m unittest discover
38
+ language: system
39
+ pass_filenames: false
40
+ files: ^(loomwork|tests)/
loomwork-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProperlyDo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.5
2
+ Name: loomwork
3
+ Version: 0.1.0
4
+ Summary: Weaves shared agent rules through your projects
5
+ Project-URL: Repository, https://github.com/ProperlyDo/agents-loomwork
6
+ Project-URL: Issues, https://github.com/ProperlyDo/agents-loomwork/issues
7
+ Author: ProperlyDo
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: AGENTS.md,agents,claude-code,opencode,rules,skills
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: click==8.5.0
24
+ Requires-Dist: questionary==2.1.1
25
+ Description-Content-Type: text/markdown
26
+
27
+ # loomwork
28
+
29
+ > **Draft.**\
30
+ > Still shaping: commands, the declaration's keys and where files land can change between releases.
31
+
32
+ Weaves shared agent rules through your projects. One repo holds the rules; `loom` puts the right ones into each project, in the place every agent reads them.
33
+
34
+ This repo is loom alone — the rules live in a repo of their own, which loom calls a source.
35
+
36
+ ## How it works
37
+
38
+ 1. **A project declares what it wants**, in a committed `.agents.toml` — `loom init` writes it from your answers:
39
+
40
+ ```toml
41
+ [[source]]
42
+ source = "git.example.com/your-team/agent-rules"
43
+ rules = ["python", "django"]
44
+ skills = ["i18n-translate"]
45
+ ```
46
+
47
+ A second `[[source]]` block adds a second rules repo — copies land in one flat directory, and a later source wins a filename it shares with an earlier one.
48
+
49
+ 2. **`loom` copies in what it names**: rules into `.agents/rules/`, skills into `.agents/skills/`. Each wired agent is then pointed at them, beside whatever the project keeps for that agent: `.claude/rules/loom` and one `.claude/skills/<name>` link per skill for Claude Code. OpenCode gets no per-project pointer: the global rules land in `~/.config/opencode/rules/`, and the source's `AGENTS.md`, copied to `~/.config/opencode/AGENTS.md`, sends it to `.agents/rules/` itself.
50
+
51
+ 3. **Sessions keep themselves fresh.** `loom init` registers a `SessionStart` hook in Claude's settings; it runs `loom sync` only where a `.agents.toml` exists, so a project that never opted in costs nothing. Each project is wired for the agents it was set up for.
52
+
53
+ ### The two files
54
+
55
+ **`.agents.toml`** sits in a project and is committed: what the project takes. One `[[source]]` block per source, each with:
56
+
57
+ - `source` — where the rules repo is: a clone url, a bare `<host>/<owner>/<repo>` with an optional folder after it when the rules sit in a subdirectory, or a path on this machine. A relative path counts from the project.
58
+ - `rules` — names from the source's `rules/<name>.md`.
59
+ - `skills` — names from the source's `skills/<name>/`.
60
+
61
+ With a single source, the header can go:
62
+
63
+ ```toml
64
+ source = "git.example.com/your-team/agent-rules"
65
+ rules = ["python", "django"]
66
+ ```
67
+
68
+ **`loom.toml`** sits in a source, beside its `rules/` and `skills/`: what the source hands out. It is optional.
69
+
70
+ ```toml
71
+ # a source this one builds on — recorded first, so this one wins a filename they share
72
+ [[source]]
73
+ source = "git.example.com/your-team/base-rules"
74
+
75
+ # what every project gets, declared or not
76
+ [global]
77
+ rules = ["git"]
78
+ skills = []
79
+ ```
80
+
81
+ A `[[source]]` here uses the same spellings as in `.agents.toml`, with a relative path counting from this source. A name under `[global]` that the source does not ship is skipped. The source's `AGENTS.md` is its universal layer, copied to each agent's user-level file — only when the source has a `loom.toml`, so a third-party repo keeps its contributor `AGENTS.md` to itself.
82
+
83
+ A source is any directory with a `rules/` or a `skills/` directory. Two spellings of one repo — a clone url, a bare `<host>/<owner>/<repo>`, a checkout on this machine — resolve to the same source, so a project declaring the url and a machine reading the checkout are using the same rules.
84
+
85
+ ## Install
86
+
87
+ macOS and Linux. On Windows, run loom, the agent and the project inside WSL — on Windows itself, loom's locks, links and hook do not work.
88
+
89
+ ```bash
90
+ uv tool install loomwork # provides `loom`
91
+ cd <project> && loom init # declares, wires, registers the hook
92
+ ```
93
+
94
+ `init` is the only command a new machine needs. It asks which agents to wire, where the rules come from — unless the project's declaration already says, in which case it takes that — and which rules and skills this project uses.
95
+
96
+ The hook calls loom by the full path `init` was started from, so a checkout run with `uv run` works as well as an install. Rerun `init` after moving either. Avoid `uvx` for `init`: its path is a cache entry that can be cleared.
97
+
98
+ From then on, opening any project that carries a `.agents.toml` wires it: the hook reads the declaration, fetches the source if this machine lacks it, and copies the rules in. Projects without one are untouched — the hook checks for the file before calling loom at all.
99
+
100
+ **Not using Claude?** `loom init` in a project does the same work by hand, and `loom sync` refreshes afterwards.
101
+
102
+ **Developing the rules themselves?** `loom init --source <rules-checkout>` points the machine at that working copy instead of a fetched clone, so edits take effect without pushing.
103
+
104
+ ### Keeping profiles apart
105
+
106
+ A profile is one Claude config dir — `~/.claude` unless `--config-dir` names another. Everything loom does for it lands there: the user-level rules and skills, the session hook, and the registry of its sources and projects. Two profiles never see each other's rules, so a second one keeps, say, a client's rules away from your own:
107
+
108
+ ```bash
109
+ loom --config-dir ~/.claude-client init # wires this project into that profile
110
+ alias claude-client='CLAUDE_CONFIG_DIR=~/.claude-client claude'
111
+ ```
112
+
113
+ Start Claude for that work through the alias. The hook carries the flag, so each session syncs only the profile it was started with. `init` asks for the dir when the flag is left out and `~/.claude` doesn't know the project yet.
114
+
115
+ - **One project, one profile.** A project's `.agents/` is shared by every profile that wires it, and each would clear the other's copies.
116
+ - **Only Claude's user-level files are per profile.** OpenCode reads `~/.config/opencode` whatever the profile, so the global rules of every profile that wires it land there together.
117
+
118
+ ### What to ignore
119
+
120
+ `loom` writes machine-local files into a project, and `init` offers to add them to its `.gitignore` when it writes the declaration. `.agents.toml` is the only file to commit:
121
+
122
+ ```gitignore
123
+ .agents/
124
+ .claude/
125
+ ```
126
+
127
+ Keep `.claude/settings.json` if the project commits shared Claude settings — ignore `.claude/*` with an exception for it instead.
128
+
129
+ ## Commands
130
+
131
+ | | |
132
+ |---|---|
133
+ | `init [project]` | set up this profile and the project: declaration, agents, wiring, and the session hook. `--agents claude,opencode` skips the question; `--source` names where the rules come from |
134
+ | `remove [project]` | unwire it and forget it |
135
+ | `sync [project]` | update its sources and rewire the project, defaulting to the current one; `--all` walks every project in this profile |
136
+ | `status` | the sources added to this profile, what it hands every project, and every wired project |
137
+ | `uninstall` | undo everything this profile installed |
138
+
139
+ ## Why copies, not links
140
+
141
+ Claude treats a symlinked project rule whose target sits outside the project as an external import, and those stay unloaded until an approval that only an `@path` import can raise — so a linked rule is silently ignored. Everything loom writes is therefore a copy, and what makes one ours to replace or delete is a trailing `<!-- written by loomwork -->` marker on a rule file, or the registry's record for a skill folder, which cannot carry one. Copying the user bridges also survives Cowork sessions, which skip a symlinked `~/.claude/CLAUDE.md`.
142
+
143
+ Links are used only where the target resolves inside the project: `.claude/rules/loom` and each `.claude/skills/<name>` onto `.agents/`. That saves writing every rule twice — once where the `.agents` standard expects it, once where the agent looks.
144
+
145
+ ## Layout
146
+
147
+ | | |
148
+ |---|---|
149
+ | `loomwork/cli.py` | argument parsing, nothing else |
150
+ | `loomwork/questions.py` | what `init` asks — one function per thing a command needs to know |
151
+ | `loomwork/prompts.py` | the terminal prompts those questions use: typed answers, checkbox picks, confirmations |
152
+ | `loomwork/loom.py` | what a command means: which rules a project takes, which agents, what gets recorded |
153
+ | `loomwork/installation.py` | the mechanism: copy, link, clear what is owned, and tell what is owned |
154
+ | `loomwork/git.py` | the only module that calls git — clone, update, read a remote |
155
+ | `loomwork/paths.py` | where things live — loom's own dirs, a project's `.agents/`, how a path is spelled or shown, and locking one |
156
+ | `loomwork/output.py` | what loom prints: `info` for steps, `warn` for what was skipped, `error` for what stopped the run |
157
+ | `loomwork/sources.py` | a rules repo on this machine: fetch it, give two spellings of it one identity, and read what a set of them defines (`Catalog`) |
158
+ | `loomwork/registry.py` | what this profile remembers — the sources added to it, its projects, what it owns where |
159
+ | `loomwork/declaration.py` | the committed `.agents.toml` and `loom.toml`, read and written |
160
+ | `loomwork/agents/` | one module per agent; each overrides only the surfaces its agent can consume |
161
+ | `tests/` | `python -m unittest discover` |
162
+
163
+ Adding an agent is one module in `loomwork/agents/` and one entry in the roster. Anything that names a directory or file format belonging to one agent belongs in that agent's module — `installation.py` stays agent-agnostic.
164
+
165
+ ## Notes
166
+
167
+ - Loom's own folder is `~/.loom`, which holds the clones it fetches (`~/.loom/sources/<host>/<owner>/<repo>`) and nothing else. Those clones are shallow and hold no work of yours, so every update resets them to their remote — a rewritten history included. A checkout of your own, added by path, is only fast-forwarded. Everything else lands where an agent reads it: global rules and skills in each agent's own `rules/` and `skills/`, the rest in the project. Loom puts nothing in `~/.agents`, since none of its agents reads from there.
168
+ - The registry lives in the config dir it bridges — `~/.claude/.loomwork.json`, or `~/.claude-work/.loomwork.json` for a second account — so each profile keeps its own sources and projects. Loom reads no env var to find it: `--config-dir` says which, and without it the profile is `~/.claude`.
169
+ - **Loom distributes where it is told.** An agent that is not installed here is wired all the same, so a machine can be set up before the agent is; `init` marks those in the picker and warns once it is done. Presence is the command on `PATH`, never the config dir — loom creates those itself.
170
+ - `init` records the agents you picked, and every later `sync` wires those. A session hook adds its own agent to them, since an agent running in a project is one it uses.
171
+ - The hook updates the sources before it copies. Claude runs it in the background, so you can type at once and only the first answer waits for it.
172
+ - Skills a source defines are copied like rules — into the project's `.agents/skills/`, or for global ones into each agent's `skills/` — and the registry records which ones we put there. The skills CLI writes into the same directories, and its entries are never ours to replace. A skill from anywhere else stays its job: `npx skills add <source> --skill <name>`.
173
+ - `.agents.toml` is what a repo weaves in — the weft. `loom.toml` is what a source strings up for whoever uses it — the warp, `[global]` included. One role each, the way git keeps `.gitmodules` and `.gitattributes` apart.
174
+ - Project-specific instructions belong in the project's own `AGENTS.md`, which loom never writes — Claude reads it natively and OpenCode directly. Beware adding your own `CLAUDE.local.md`: its presence stops Claude reading `AGENTS.md` natively, unless it imports `@AGENTS.md` or **Project instructions** is set to `claude-md-and-agents-md`.
175
+
176
+ ## Open
177
+
178
+ - **`remove` does not stick.** A project that still carries `.agents.toml` is wired again by the next session hook; deleting the file is the opt-out that holds.
179
+ - **Colliding names pass silently.** Two sources shipping one filename merge by the declared order, last one winning, and nothing says it happened. Skills have no scope to fall back on either: Claude discovers them one level deep (`skills/<name>/SKILL.md`), so a shared name is a shared folder.
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ git clone git@github.com:ProperlyDo/agents-loomwork.git && cd agents-loomwork
185
+ uv sync # creates .venv, installs click and questionary, and this package, editable
186
+ uv run python -m unittest discover
187
+ ```
188
+
189
+ Run loom itself the same way, from whichever project you are testing against:
190
+
191
+ ```bash
192
+ cd <project>
193
+ uv run --project <path-to-this-clone> loom init
194
+ ```
195
+
196
+ Both forms run your working copy, so an edit takes effect on the next invocation — no install, no reinstall. `source .venv/bin/activate` in this repo is the alternative: it puts `loom` and `python` on PATH until you `deactivate`, after which `loom init` works from any directory without the `--project` flag.
197
+
198
+ Tests need nothing but the venv and never touch your home: every case builds its source, project and agent config dirs under a temp directory, through `SandboxTestCase` in `tests/fixtures.py`. They run no git either: `tests/__init__.py` makes any git call a test did not patch fail loudly, so a test that needs an answer from git patches the function that gives it.
199
+
200
+ To release, raise the version and push it to `main`: `uv version --bump patch` (or `minor`, `major`), commit, push. The Release workflow tests, publishes to PyPI, tags `v<version>` and creates the GitHub release; a push that leaves the version alone releases nothing.
@@ -0,0 +1,174 @@
1
+ # loomwork
2
+
3
+ > **Draft.**\
4
+ > Still shaping: commands, the declaration's keys and where files land can change between releases.
5
+
6
+ Weaves shared agent rules through your projects. One repo holds the rules; `loom` puts the right ones into each project, in the place every agent reads them.
7
+
8
+ This repo is loom alone — the rules live in a repo of their own, which loom calls a source.
9
+
10
+ ## How it works
11
+
12
+ 1. **A project declares what it wants**, in a committed `.agents.toml` — `loom init` writes it from your answers:
13
+
14
+ ```toml
15
+ [[source]]
16
+ source = "git.example.com/your-team/agent-rules"
17
+ rules = ["python", "django"]
18
+ skills = ["i18n-translate"]
19
+ ```
20
+
21
+ A second `[[source]]` block adds a second rules repo — copies land in one flat directory, and a later source wins a filename it shares with an earlier one.
22
+
23
+ 2. **`loom` copies in what it names**: rules into `.agents/rules/`, skills into `.agents/skills/`. Each wired agent is then pointed at them, beside whatever the project keeps for that agent: `.claude/rules/loom` and one `.claude/skills/<name>` link per skill for Claude Code. OpenCode gets no per-project pointer: the global rules land in `~/.config/opencode/rules/`, and the source's `AGENTS.md`, copied to `~/.config/opencode/AGENTS.md`, sends it to `.agents/rules/` itself.
24
+
25
+ 3. **Sessions keep themselves fresh.** `loom init` registers a `SessionStart` hook in Claude's settings; it runs `loom sync` only where a `.agents.toml` exists, so a project that never opted in costs nothing. Each project is wired for the agents it was set up for.
26
+
27
+ ### The two files
28
+
29
+ **`.agents.toml`** sits in a project and is committed: what the project takes. One `[[source]]` block per source, each with:
30
+
31
+ - `source` — where the rules repo is: a clone url, a bare `<host>/<owner>/<repo>` with an optional folder after it when the rules sit in a subdirectory, or a path on this machine. A relative path counts from the project.
32
+ - `rules` — names from the source's `rules/<name>.md`.
33
+ - `skills` — names from the source's `skills/<name>/`.
34
+
35
+ With a single source, the header can go:
36
+
37
+ ```toml
38
+ source = "git.example.com/your-team/agent-rules"
39
+ rules = ["python", "django"]
40
+ ```
41
+
42
+ **`loom.toml`** sits in a source, beside its `rules/` and `skills/`: what the source hands out. It is optional.
43
+
44
+ ```toml
45
+ # a source this one builds on — recorded first, so this one wins a filename they share
46
+ [[source]]
47
+ source = "git.example.com/your-team/base-rules"
48
+
49
+ # what every project gets, declared or not
50
+ [global]
51
+ rules = ["git"]
52
+ skills = []
53
+ ```
54
+
55
+ A `[[source]]` here uses the same spellings as in `.agents.toml`, with a relative path counting from this source. A name under `[global]` that the source does not ship is skipped. The source's `AGENTS.md` is its universal layer, copied to each agent's user-level file — only when the source has a `loom.toml`, so a third-party repo keeps its contributor `AGENTS.md` to itself.
56
+
57
+ A source is any directory with a `rules/` or a `skills/` directory. Two spellings of one repo — a clone url, a bare `<host>/<owner>/<repo>`, a checkout on this machine — resolve to the same source, so a project declaring the url and a machine reading the checkout are using the same rules.
58
+
59
+ ## Install
60
+
61
+ macOS and Linux. On Windows, run loom, the agent and the project inside WSL — on Windows itself, loom's locks, links and hook do not work.
62
+
63
+ ```bash
64
+ uv tool install loomwork # provides `loom`
65
+ cd <project> && loom init # declares, wires, registers the hook
66
+ ```
67
+
68
+ `init` is the only command a new machine needs. It asks which agents to wire, where the rules come from — unless the project's declaration already says, in which case it takes that — and which rules and skills this project uses.
69
+
70
+ The hook calls loom by the full path `init` was started from, so a checkout run with `uv run` works as well as an install. Rerun `init` after moving either. Avoid `uvx` for `init`: its path is a cache entry that can be cleared.
71
+
72
+ From then on, opening any project that carries a `.agents.toml` wires it: the hook reads the declaration, fetches the source if this machine lacks it, and copies the rules in. Projects without one are untouched — the hook checks for the file before calling loom at all.
73
+
74
+ **Not using Claude?** `loom init` in a project does the same work by hand, and `loom sync` refreshes afterwards.
75
+
76
+ **Developing the rules themselves?** `loom init --source <rules-checkout>` points the machine at that working copy instead of a fetched clone, so edits take effect without pushing.
77
+
78
+ ### Keeping profiles apart
79
+
80
+ A profile is one Claude config dir — `~/.claude` unless `--config-dir` names another. Everything loom does for it lands there: the user-level rules and skills, the session hook, and the registry of its sources and projects. Two profiles never see each other's rules, so a second one keeps, say, a client's rules away from your own:
81
+
82
+ ```bash
83
+ loom --config-dir ~/.claude-client init # wires this project into that profile
84
+ alias claude-client='CLAUDE_CONFIG_DIR=~/.claude-client claude'
85
+ ```
86
+
87
+ Start Claude for that work through the alias. The hook carries the flag, so each session syncs only the profile it was started with. `init` asks for the dir when the flag is left out and `~/.claude` doesn't know the project yet.
88
+
89
+ - **One project, one profile.** A project's `.agents/` is shared by every profile that wires it, and each would clear the other's copies.
90
+ - **Only Claude's user-level files are per profile.** OpenCode reads `~/.config/opencode` whatever the profile, so the global rules of every profile that wires it land there together.
91
+
92
+ ### What to ignore
93
+
94
+ `loom` writes machine-local files into a project, and `init` offers to add them to its `.gitignore` when it writes the declaration. `.agents.toml` is the only file to commit:
95
+
96
+ ```gitignore
97
+ .agents/
98
+ .claude/
99
+ ```
100
+
101
+ Keep `.claude/settings.json` if the project commits shared Claude settings — ignore `.claude/*` with an exception for it instead.
102
+
103
+ ## Commands
104
+
105
+ | | |
106
+ |---|---|
107
+ | `init [project]` | set up this profile and the project: declaration, agents, wiring, and the session hook. `--agents claude,opencode` skips the question; `--source` names where the rules come from |
108
+ | `remove [project]` | unwire it and forget it |
109
+ | `sync [project]` | update its sources and rewire the project, defaulting to the current one; `--all` walks every project in this profile |
110
+ | `status` | the sources added to this profile, what it hands every project, and every wired project |
111
+ | `uninstall` | undo everything this profile installed |
112
+
113
+ ## Why copies, not links
114
+
115
+ Claude treats a symlinked project rule whose target sits outside the project as an external import, and those stay unloaded until an approval that only an `@path` import can raise — so a linked rule is silently ignored. Everything loom writes is therefore a copy, and what makes one ours to replace or delete is a trailing `<!-- written by loomwork -->` marker on a rule file, or the registry's record for a skill folder, which cannot carry one. Copying the user bridges also survives Cowork sessions, which skip a symlinked `~/.claude/CLAUDE.md`.
116
+
117
+ Links are used only where the target resolves inside the project: `.claude/rules/loom` and each `.claude/skills/<name>` onto `.agents/`. That saves writing every rule twice — once where the `.agents` standard expects it, once where the agent looks.
118
+
119
+ ## Layout
120
+
121
+ | | |
122
+ |---|---|
123
+ | `loomwork/cli.py` | argument parsing, nothing else |
124
+ | `loomwork/questions.py` | what `init` asks — one function per thing a command needs to know |
125
+ | `loomwork/prompts.py` | the terminal prompts those questions use: typed answers, checkbox picks, confirmations |
126
+ | `loomwork/loom.py` | what a command means: which rules a project takes, which agents, what gets recorded |
127
+ | `loomwork/installation.py` | the mechanism: copy, link, clear what is owned, and tell what is owned |
128
+ | `loomwork/git.py` | the only module that calls git — clone, update, read a remote |
129
+ | `loomwork/paths.py` | where things live — loom's own dirs, a project's `.agents/`, how a path is spelled or shown, and locking one |
130
+ | `loomwork/output.py` | what loom prints: `info` for steps, `warn` for what was skipped, `error` for what stopped the run |
131
+ | `loomwork/sources.py` | a rules repo on this machine: fetch it, give two spellings of it one identity, and read what a set of them defines (`Catalog`) |
132
+ | `loomwork/registry.py` | what this profile remembers — the sources added to it, its projects, what it owns where |
133
+ | `loomwork/declaration.py` | the committed `.agents.toml` and `loom.toml`, read and written |
134
+ | `loomwork/agents/` | one module per agent; each overrides only the surfaces its agent can consume |
135
+ | `tests/` | `python -m unittest discover` |
136
+
137
+ Adding an agent is one module in `loomwork/agents/` and one entry in the roster. Anything that names a directory or file format belonging to one agent belongs in that agent's module — `installation.py` stays agent-agnostic.
138
+
139
+ ## Notes
140
+
141
+ - Loom's own folder is `~/.loom`, which holds the clones it fetches (`~/.loom/sources/<host>/<owner>/<repo>`) and nothing else. Those clones are shallow and hold no work of yours, so every update resets them to their remote — a rewritten history included. A checkout of your own, added by path, is only fast-forwarded. Everything else lands where an agent reads it: global rules and skills in each agent's own `rules/` and `skills/`, the rest in the project. Loom puts nothing in `~/.agents`, since none of its agents reads from there.
142
+ - The registry lives in the config dir it bridges — `~/.claude/.loomwork.json`, or `~/.claude-work/.loomwork.json` for a second account — so each profile keeps its own sources and projects. Loom reads no env var to find it: `--config-dir` says which, and without it the profile is `~/.claude`.
143
+ - **Loom distributes where it is told.** An agent that is not installed here is wired all the same, so a machine can be set up before the agent is; `init` marks those in the picker and warns once it is done. Presence is the command on `PATH`, never the config dir — loom creates those itself.
144
+ - `init` records the agents you picked, and every later `sync` wires those. A session hook adds its own agent to them, since an agent running in a project is one it uses.
145
+ - The hook updates the sources before it copies. Claude runs it in the background, so you can type at once and only the first answer waits for it.
146
+ - Skills a source defines are copied like rules — into the project's `.agents/skills/`, or for global ones into each agent's `skills/` — and the registry records which ones we put there. The skills CLI writes into the same directories, and its entries are never ours to replace. A skill from anywhere else stays its job: `npx skills add <source> --skill <name>`.
147
+ - `.agents.toml` is what a repo weaves in — the weft. `loom.toml` is what a source strings up for whoever uses it — the warp, `[global]` included. One role each, the way git keeps `.gitmodules` and `.gitattributes` apart.
148
+ - Project-specific instructions belong in the project's own `AGENTS.md`, which loom never writes — Claude reads it natively and OpenCode directly. Beware adding your own `CLAUDE.local.md`: its presence stops Claude reading `AGENTS.md` natively, unless it imports `@AGENTS.md` or **Project instructions** is set to `claude-md-and-agents-md`.
149
+
150
+ ## Open
151
+
152
+ - **`remove` does not stick.** A project that still carries `.agents.toml` is wired again by the next session hook; deleting the file is the opt-out that holds.
153
+ - **Colliding names pass silently.** Two sources shipping one filename merge by the declared order, last one winning, and nothing says it happened. Skills have no scope to fall back on either: Claude discovers them one level deep (`skills/<name>/SKILL.md`), so a shared name is a shared folder.
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ git clone git@github.com:ProperlyDo/agents-loomwork.git && cd agents-loomwork
159
+ uv sync # creates .venv, installs click and questionary, and this package, editable
160
+ uv run python -m unittest discover
161
+ ```
162
+
163
+ Run loom itself the same way, from whichever project you are testing against:
164
+
165
+ ```bash
166
+ cd <project>
167
+ uv run --project <path-to-this-clone> loom init
168
+ ```
169
+
170
+ Both forms run your working copy, so an edit takes effect on the next invocation — no install, no reinstall. `source .venv/bin/activate` in this repo is the alternative: it puts `loom` and `python` on PATH until you `deactivate`, after which `loom init` works from any directory without the `--project` flag.
171
+
172
+ Tests need nothing but the venv and never touch your home: every case builds its source, project and agent config dirs under a temp directory, through `SandboxTestCase` in `tests/fixtures.py`. They run no git either: `tests/__init__.py` makes any git call a test did not patch fail loudly, so a test that needs an answer from git patches the function that gives it.
173
+
174
+ To release, raise the version and push it to `main`: `uv version --bump patch` (or `minor`, `major`), commit, push. The Release workflow tests, publishes to PyPI, tags `v<version>` and creates the GitHub release; a push that leaves the version alone releases nothing.
@@ -0,0 +1,4 @@
1
+ # one rename moves the copy marker and the registry file together
2
+ PACKAGE_NAME = __name__
3
+ # what a user types, while the package keeps its own name
4
+ COMMAND_NAME = "loom"
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from loomwork.agents.base import Agent
6
+ from loomwork.agents.claude_code import ClaudeCode
7
+ from loomwork.agents.opencode import OpenCode
8
+
9
+
10
+ def roster(claude_config_dir: Path) -> tuple[Agent, ...]:
11
+ """Every supported agent, in the order a machine converges them — Claude in the config dir of this profile."""
12
+ return (ClaudeCode(claude_config_dir), OpenCode())
13
+
14
+
15
+ __all__ = ["Agent", "ClaudeCode", "OpenCode", "roster"]