Contributing¶
Attaché stays small so anyone can read the core in an afternoon and change it with confidence.
Accepted in the core: bug fixes, security fixes, simplifications, less code.
Not accepted in the core: features, integrations, channels, file formats. These should be skills. (This rule comes from nanoclaw.)
The source is at OpenAfterHours/attache. The design is in the proposal; read its architecture and code budget sections first.
Dev setup¶
You need uv and git. Then:
uv sync # the dev environment
uv run pytest # all tests, including the size budget
uv run ruff check . && uv run ruff format --check . # lint and format
uv run attache --help # the CLI, from source
To try your changes without touching your real ~/.attache, give every command a scratch
home:
Layout¶
src/attache/
types.py shared dataclasses and protocols; every module talks through these
app.py builds Services and runs everything
config.py attache.toml -> Config; init_home()
store.py SQLite: messages, docs, tasks, sessions
skills.py skill discovery, prompt index, git commit/log/undo of <home>
scheduler.py fires due tasks
agent.py shared tool loop for API backends
backends/ anthropic, openai (chat + responses), claude_cli
tools.py the tools a turn may use, filtered by role
router.py inbound message -> access, trigger, role -> turn -> reply
channels.py channel manager, terminal channel
web.py Starlette app: chat page, artifacts, data bridge, SSE, MCP endpoint
cli.py argparse entry point
static/ ui.html, bridge.js
bundled/ attache.toml template, memory templates, bundled skills (copied by init)
Rules¶
- Size is a test.
tests/test_budget.pyfails above 2,710 lines of code insrc/attache(including the owner settings service). Blank lines, comments and docstrings don't count, so never make code denser to fit: write the readable version. If a feature doesn't fit, it belongs in a skill. - No new runtime dependencies without discussion. Skill scripts declare their own with
PEP 723 headers and run through
uv run. - Tests run offline. No network, no API keys, no real
claudebinary. Inject fakes: every backend takes aclientorrunnerargument for this. - Cross-platform. Windows, macOS and Linux. Use pathlib,
encoding="utf-8"on every text read and write, argument lists for subprocesses (no shell, except in theruntool), and no POSIX-only APIs. - Style. ruff with the repo config,
from __future__ import annotations, type hints on public functions, short docstrings on public functions only, and comments only for a non-obvious why. Plain functions and dataclasses over class hierarchies. - Security model. Changes must keep the rules in Security: roles and
their tools, fenced untrusted text, sandboxed pages, loopback only, and
claude-clifor the owner's own chats only. - Contracts live in
types.pyand the module docstrings. Change a public signature only together with every caller and its tests.
Adding a skill¶
A skill is a folder with a SKILL.md in the
Agent Skills format:
skills/<name>/
SKILL.md frontmatter: name, description, optional metadata
scripts/*.py optional; PEP 723 header declares dependencies, run with `uv run`
templates/ optional; artifact templates
Metadata keys Attaché reads:
attache-roles: "member"makes the skill available in group chats: members see its full text in their prompt and may run its scripts. Without it a skill is owner-only.attache-channel: "<command>"marks a channel sidecar (phase 2).
Bundled skills live in src/attache/bundled/skills/ and are copied into a user's home by
attache init. Each bundled skill's scripts are capped at 300 lines of code by
tests/test_budget.py (1,390 for mail, 310 for tui). The Skills guide covers writing one in full.
The size budget¶
tests/test_budget.py fails when the core passes 2,710 lines of code. Blank lines, comments
and docstrings don't count, so write the clearest version of the code, not the shortest.
Bundled skills, static files and tests don't count toward it.
Working on these docs¶
The site is built with Zensical from docs/ and zensical.toml, in
the OpenAfterHours night-sky theme shared with the other OpenAfterHours projects. The docs
dependency group has what it needs:
uv run --group docs zensical serve # preview at http://localhost:8000, live reload
uv run --group docs zensical build --strict # the static site, in ./site, as CI builds it
--strict fails the build on a link to a missing page or heading.
- Pages are Markdown files in
docs/. Add a new one tonavinzensical.toml. docs/assets/stylesheets/oah-theme.cssholds the theme. Its brand tokens are copied from rwa_calculator'stokens.css, the single source of truth for the OpenAfterHours palette: change them there first.docs/proposal.htmlis the design proposal, a standalone HTML page that the build copies as it is.
Every pull request builds the site, and every push to main publishes it to GitHub Pages.
Before you open a pull request¶
CI runs the same checks, the tests on Windows, macOS and Linux with Python 3.11, 3.12 and
3.13, and a smoke test of the built wheel. It also sets UV_LOCKED=1, so after changing a
dependency, run uv lock and commit uv.lock.