Local-first planning ledger for human and agentic work
Project description
ForgeWire Anvil
A local-first planning ledger for human and agentic work.
Projects, issues, boards, sprints, workflows, and typed relations — provider neutral, auditable, dependency-free, and fully usable with no network and no external issue-tracker account.
pip install forgewire-anvil
anvil project create PF "Platform"
anvil issue create PF "Ship the thing" --priority high --label release
anvil transition PF-1 todo
anvil relation add PF-1 fabric_task task-42 --label "Build"
anvil validate
from forgewire_anvil import Anvil, IssueRelation
anvil = Anvil() # user-scoped ledger; honors ANVIL_DATA_DIR
await anvil.provider.create_project(key="PF", name="Platform")
issue = await anvil.provider.create_issue(project_key="PF", summary="Ship it")
await anvil.provider.add_relation(issue.key, IssueRelation(
source_key=issue.key, target_type="fabric_task", target_id="task-42",
))
Status: developer preview (
0.1.0a0). The API and on-disk schema may change between releases — pin an exact version. The JSON store is single-writer; read docs/limitations.md before pointing more than one process at a ledger.
Why
Planning tools assume a server, an account, and a human. Anvil assumes none of them. It is a ledger you own, on disk, in plain JSON you can read — designed so agents and people can plan against the same records without a hosted service in the middle.
Being local-first is the point, not a limitation:
- Yours. Plain JSON in a directory you choose. No account, no telemetry, no network calls. Ever.
- Durable. Atomic writes, schema versioning, and lossless export/import. Fields a newer build wrote survive an older build reading and re-saving them.
- Provider-neutral.
IssueTrackerProviderandIssueStoreare protocols; the built-in provider and JSON store are just the default implementations. - Dependency-free. No runtime dependencies. The CLI is stdlib
argparse.
What Anvil owns — and what it doesn't
Anvil owns projects, issues, planning lifecycle, workflows and transitions, boards and sprints, comments and activity history, issue-to-issue links, typed relations to external work objects, local storage, schema migration, and import/export.
Anvil does not own execution. It links to and summarises execution records; it is not the execution authority, and its activity log is a convenience history rather than an audit trail. Signed remote execution, host execution, repository governance, and approval workflows belong to the systems that own them.
That boundary is the design: cross-system references are typed relations and summarised events, never duplicate authoritative records.
Where data lives
Resolved in this order:
- an explicit path (
Anvil("/path"), oranvil --data-dir /path); - the
ANVIL_DATA_DIRenvironment variable; - a per-user platform directory:
- Windows —
%LOCALAPPDATA%\ForgeWire\Anvil - macOS —
~/Library/Application Support/ForgeWire/Anvil - Linux/BSD —
$XDG_DATA_HOME/forgewire/anvil(else~/.local/share/forgewire/anvil)
- Windows —
There is deliberately no working-directory-relative default: an operational ledger that follows your shell around scatters itself across checkouts.
Each project is a directory of human-readable JSON:
<data-dir>/
├── .anvil-store.json # schema version marker
└── PF/
├── project.json # metadata + next-issue counter
├── issues.json # every issue, with comments, links, relations, history
└── boards.json # boards, with embedded sprints
Already have a ledger somewhere else? anvil migrate store <source> adopts it
losslessly and only ever reads the source.
Errors
Anvil raises; it does not wrap. Every failure is an IssueTrackerError subclass
carrying a stable error_code (ISSUE_NOT_FOUND, INVALID_TRANSITION,
VALIDATION_ERROR, …), so an embedding application maps errors once at its own
boundary instead of unwrapping a second envelope.
The CLI surfaces the same contract: exit 0 on success, 1 on a
planning-domain error (with its code on stderr), 2 on a usage error. Add
--json to any command for machine-readable output.
Relations vs. typed_links
Issue.relations is the forward canonical field for provider-neutral links to
non-issue work objects. Issue.typed_links is an earlier name for the same
records, kept as a compatibility read window and still written as a mirror,
so downlevel readers keep working. Legacy typed_links-only data upgrades via:
anvil migrate relations
The migration is lossless, idempotent, and preserves unknown fields. No
deprecation of typed_links is scheduled; it will not be removed without a
migration path and a stated window.
Development
pytest # no install needed: pyproject sets pythonpath
python -m build
Requires Python 3.11+. Tested against CPython 3.11, 3.12, and 3.13.
Contributing
Anvil is developed in ForgeWire's private monorepo and published here as a mirror, so this repository does not accept pull requests — a sync would overwrite them. Issues and discussion are welcome and read.
Licence
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file forgewire_anvil-0.1.0a0.tar.gz.
File metadata
- Download URL: forgewire_anvil-0.1.0a0.tar.gz
- Upload date:
- Size: 54.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c8f374ad73cc75ddbddaf036c50d99d79a5975129928c5bbc1f8bafa563a7d1
|
|
| MD5 |
61826a47cd917e52c60b14d81665cca9
|
|
| BLAKE2b-256 |
f43d77043067ed0d23e4372febd44708708fc5f3222ca60606cbb7d819bc06e7
|
File details
Details for the file forgewire_anvil-0.1.0a0-py3-none-any.whl.
File metadata
- Download URL: forgewire_anvil-0.1.0a0-py3-none-any.whl
- Upload date:
- Size: 41.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41f327a8422e03dff7e40d5a515bd6f2429a508c212bfe1eff13c40c36449c92
|
|
| MD5 |
d473220f7bc2b2bcb3366073aa7276ed
|
|
| BLAKE2b-256 |
7a19d7e3e0c1d4b29ca9318a8bfc2eb3eff81fce9c66ee8bf7c7b790f84c5503
|