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.
- loomwork-0.1.0/.agents.toml +2 -0
- loomwork-0.1.0/.github/workflows/release.yaml +110 -0
- loomwork-0.1.0/.gitignore +7 -0
- loomwork-0.1.0/.pre-commit-config.yaml +40 -0
- loomwork-0.1.0/LICENSE +21 -0
- loomwork-0.1.0/PKG-INFO +200 -0
- loomwork-0.1.0/README.md +174 -0
- loomwork-0.1.0/loomwork/__init__.py +4 -0
- loomwork-0.1.0/loomwork/agents/__init__.py +15 -0
- loomwork-0.1.0/loomwork/agents/base.py +153 -0
- loomwork-0.1.0/loomwork/agents/claude_code.py +110 -0
- loomwork-0.1.0/loomwork/agents/opencode.py +20 -0
- loomwork-0.1.0/loomwork/cli.py +103 -0
- loomwork-0.1.0/loomwork/declaration.py +120 -0
- loomwork-0.1.0/loomwork/git.py +66 -0
- loomwork-0.1.0/loomwork/installation.py +172 -0
- loomwork-0.1.0/loomwork/loom.py +353 -0
- loomwork-0.1.0/loomwork/output.py +51 -0
- loomwork-0.1.0/loomwork/paths.py +68 -0
- loomwork-0.1.0/loomwork/prompts.py +83 -0
- loomwork-0.1.0/loomwork/questions.py +115 -0
- loomwork-0.1.0/loomwork/registry.py +104 -0
- loomwork-0.1.0/loomwork/sources.py +193 -0
- loomwork-0.1.0/pyproject.toml +70 -0
- loomwork-0.1.0/tests/__init__.py +4 -0
- loomwork-0.1.0/tests/fixtures.py +78 -0
- loomwork-0.1.0/tests/test_installation.py +452 -0
- loomwork-0.1.0/tests/test_loom.py +340 -0
- loomwork-0.1.0/tests/test_registry.py +88 -0
- loomwork-0.1.0/tests/test_sources.py +258 -0
- loomwork-0.1.0/uv.lock +87 -0
|
@@ -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,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.
|
loomwork-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
loomwork-0.1.0/README.md
ADDED
|
@@ -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,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"]
|