halyard 0.0.1__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.
- halyard-0.0.1/.github/workflows/ci.yml +38 -0
- halyard-0.0.1/.gitignore +33 -0
- halyard-0.0.1/LICENSE +21 -0
- halyard-0.0.1/PKG-INFO +88 -0
- halyard-0.0.1/README.md +54 -0
- halyard-0.0.1/openspec/changes/v0-time-and-invoice/design.md +120 -0
- halyard-0.0.1/openspec/changes/v0-time-and-invoice/proposal.md +35 -0
- halyard-0.0.1/openspec/changes/v0-time-and-invoice/specs/cli.md +116 -0
- halyard-0.0.1/openspec/changes/v0-time-and-invoice/specs/data-model.md +115 -0
- halyard-0.0.1/openspec/changes/v0-time-and-invoice/tasks.md +64 -0
- halyard-0.0.1/openspec/project.md +79 -0
- halyard-0.0.1/prompts/system.md +76 -0
- halyard-0.0.1/pyproject.toml +71 -0
- halyard-0.0.1/src/halyard/__init__.py +3 -0
- halyard-0.0.1/src/halyard/__main__.py +4 -0
- halyard-0.0.1/src/halyard/cli.py +82 -0
- halyard-0.0.1/templates/invoice.md.j2 +43 -0
- halyard-0.0.1/tests/__init__.py +0 -0
- halyard-0.0.1/tests/test_cli_smoke.py +24 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Lint with ruff
|
|
29
|
+
run: ruff check .
|
|
30
|
+
|
|
31
|
+
- name: Format check with ruff
|
|
32
|
+
run: ruff format --check .
|
|
33
|
+
|
|
34
|
+
- name: Type-check with mypy
|
|
35
|
+
run: mypy src
|
|
36
|
+
|
|
37
|
+
- name: Test with pytest
|
|
38
|
+
run: pytest --cov=halyard --cov-report=term-missing
|
halyard-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
|
|
17
|
+
# Environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
*.swp
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# Halyard runtime
|
|
29
|
+
.halyard-cache/
|
|
30
|
+
.halyard/active
|
|
31
|
+
|
|
32
|
+
# OS
|
|
33
|
+
Thumbs.db
|
halyard-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kormilo LLC
|
|
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.
|
halyard-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: halyard
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Plain-text, agent-native financial OS for freelancers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Kormiloio/Halyard
|
|
6
|
+
Project-URL: Repository, https://github.com/Kormiloio/Halyard
|
|
7
|
+
Project-URL: Issues, https://github.com/Kormiloio/Halyard/issues
|
|
8
|
+
Author: Kormilo LLC
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: accounting,agent,claude,freelance,invoicing,plaintext,time-tracking
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: anthropic>=0.40
|
|
21
|
+
Requires-Dist: dateparser>=1.2
|
|
22
|
+
Requires-Dist: jinja2>=3.1
|
|
23
|
+
Requires-Dist: pydantic>=2.6
|
|
24
|
+
Requires-Dist: rich>=13.7
|
|
25
|
+
Requires-Dist: tomli-w>=1.0
|
|
26
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
27
|
+
Requires-Dist: typer>=0.12
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Halyard
|
|
36
|
+
|
|
37
|
+
> *A halyard is the line that raises the sails. Pull on it, the sails go up. Pull on this one, your books go up.*
|
|
38
|
+
|
|
39
|
+
A plain-text, agent-native financial OS for freelancers. Your time, expenses, and invoices live as plain text on your laptop. An AI agent (Claude) reads and writes them on your behalf.
|
|
40
|
+
|
|
41
|
+
**Status:** pre-alpha. v0 in development. Watch the repo for the first release.
|
|
42
|
+
|
|
43
|
+
## The pitch
|
|
44
|
+
|
|
45
|
+
You run a freelance business. Your time tracking is in Notion or Toggl. Your invoices are in FreshBooks or QuickBooks. Your expenses are CSVs you've been meaning to deal with. Your data is locked in five different SaaS subscriptions, none of which talk to each other, all of which get more expensive every year.
|
|
46
|
+
|
|
47
|
+
Halyard is an alternative:
|
|
48
|
+
|
|
49
|
+
- **Local-first.** Everything lives in a folder on your computer. Git it, back it up, sync it however you want.
|
|
50
|
+
- **Plain text forever.** [hledger timeclock](https://hledger.org/timeclock.html) for time, [Beancount](https://beancount.github.io/) for ledgers, markdown for invoices. No proprietary formats. Compatible with the entire plaintext-accounting ecosystem on day one.
|
|
51
|
+
- **Agent-native.** "I worked 3 hours on ACME this morning" → entry written. "Draft an invoice for last month" → markdown + PDF generated. The agent edits the same files you can edit by hand.
|
|
52
|
+
- **Open source, MIT.** Yours forever.
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
> Coming with v0.1.0. See [`openspec/changes/v0-time-and-invoice/`](./openspec/changes/v0-time-and-invoice/) for what's being built.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pipx install halyard
|
|
60
|
+
cd ~/businesses/my-freelance
|
|
61
|
+
halyard init
|
|
62
|
+
halyard
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How it's being built
|
|
66
|
+
|
|
67
|
+
This project uses [OpenSpec](https://github.com/Fission-AI/OpenSpec) for spec-driven development. Every feature lives as a change folder under `openspec/changes/` with a proposal, specs, design, and tasks.
|
|
68
|
+
|
|
69
|
+
To see what's being built next, look at any folder under `openspec/changes/` that hasn't been moved to `openspec/changes/archive/`.
|
|
70
|
+
|
|
71
|
+
## Roadmap
|
|
72
|
+
|
|
73
|
+
- **v0** — time tracking + invoicing CLI + Claude REPL. *In progress.*
|
|
74
|
+
- **v1** — expense ingestion, local web UI at `localhost:7474`, plugin/skill system.
|
|
75
|
+
- **v2** — bank sync (Plaid), full bookkeeping automation, project profitability.
|
|
76
|
+
- **v3+** — tax forms, e-file. Distant future.
|
|
77
|
+
|
|
78
|
+
## Contributing
|
|
79
|
+
|
|
80
|
+
Too early. Star the repo and watch for v0.1.0.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
A [Kormilo LLC](https://kormilo.io) project. *Kormilo* is Slavic for "rudder" — the helm of your business. Halyard is the first thing it builds.
|
halyard-0.0.1/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Halyard
|
|
2
|
+
|
|
3
|
+
> *A halyard is the line that raises the sails. Pull on it, the sails go up. Pull on this one, your books go up.*
|
|
4
|
+
|
|
5
|
+
A plain-text, agent-native financial OS for freelancers. Your time, expenses, and invoices live as plain text on your laptop. An AI agent (Claude) reads and writes them on your behalf.
|
|
6
|
+
|
|
7
|
+
**Status:** pre-alpha. v0 in development. Watch the repo for the first release.
|
|
8
|
+
|
|
9
|
+
## The pitch
|
|
10
|
+
|
|
11
|
+
You run a freelance business. Your time tracking is in Notion or Toggl. Your invoices are in FreshBooks or QuickBooks. Your expenses are CSVs you've been meaning to deal with. Your data is locked in five different SaaS subscriptions, none of which talk to each other, all of which get more expensive every year.
|
|
12
|
+
|
|
13
|
+
Halyard is an alternative:
|
|
14
|
+
|
|
15
|
+
- **Local-first.** Everything lives in a folder on your computer. Git it, back it up, sync it however you want.
|
|
16
|
+
- **Plain text forever.** [hledger timeclock](https://hledger.org/timeclock.html) for time, [Beancount](https://beancount.github.io/) for ledgers, markdown for invoices. No proprietary formats. Compatible with the entire plaintext-accounting ecosystem on day one.
|
|
17
|
+
- **Agent-native.** "I worked 3 hours on ACME this morning" → entry written. "Draft an invoice for last month" → markdown + PDF generated. The agent edits the same files you can edit by hand.
|
|
18
|
+
- **Open source, MIT.** Yours forever.
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
> Coming with v0.1.0. See [`openspec/changes/v0-time-and-invoice/`](./openspec/changes/v0-time-and-invoice/) for what's being built.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pipx install halyard
|
|
26
|
+
cd ~/businesses/my-freelance
|
|
27
|
+
halyard init
|
|
28
|
+
halyard
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How it's being built
|
|
32
|
+
|
|
33
|
+
This project uses [OpenSpec](https://github.com/Fission-AI/OpenSpec) for spec-driven development. Every feature lives as a change folder under `openspec/changes/` with a proposal, specs, design, and tasks.
|
|
34
|
+
|
|
35
|
+
To see what's being built next, look at any folder under `openspec/changes/` that hasn't been moved to `openspec/changes/archive/`.
|
|
36
|
+
|
|
37
|
+
## Roadmap
|
|
38
|
+
|
|
39
|
+
- **v0** — time tracking + invoicing CLI + Claude REPL. *In progress.*
|
|
40
|
+
- **v1** — expense ingestion, local web UI at `localhost:7474`, plugin/skill system.
|
|
41
|
+
- **v2** — bank sync (Plaid), full bookkeeping automation, project profitability.
|
|
42
|
+
- **v3+** — tax forms, e-file. Distant future.
|
|
43
|
+
|
|
44
|
+
## Contributing
|
|
45
|
+
|
|
46
|
+
Too early. Star the repo and watch for v0.1.0.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
A [Kormilo LLC](https://kormilo.io) project. *Kormilo* is Slavic for "rudder" — the helm of your business. Halyard is the first thing it builds.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
## Stack
|
|
4
|
+
|
|
5
|
+
- **Language:** Python 3.11+ (Typer for CLI, Rich for terminal output)
|
|
6
|
+
- **Agent:** Anthropic SDK direct, single-turn tool-use loop
|
|
7
|
+
- **Models:** Pydantic v2 for config schemas
|
|
8
|
+
- **Templating:** Jinja2
|
|
9
|
+
- **PDF:** typst via subprocess (cleaner output than weasyprint, single binary)
|
|
10
|
+
- **Time parsing:** dateparser
|
|
11
|
+
- **Tests:** pytest, with golden-file tests for invoice rendering
|
|
12
|
+
|
|
13
|
+
### Why Python?
|
|
14
|
+
|
|
15
|
+
Largest contributor pool for AI tooling, easy install via `pipx`, fast
|
|
16
|
+
iteration. We can rewrite hot paths or distribute as a Rust binary later
|
|
17
|
+
if real performance demands it. Day-one priority is contributor velocity,
|
|
18
|
+
not microbenchmark wins.
|
|
19
|
+
|
|
20
|
+
### Why typst over weasyprint?
|
|
21
|
+
|
|
22
|
+
typst produces consistently good-looking PDFs out of the box, compiles in
|
|
23
|
+
milliseconds, and has a clean templating language that we can expose to
|
|
24
|
+
power users later as an alternative invoice format. weasyprint requires
|
|
25
|
+
HTML+CSS to look professional, and the output quality varies. typst is a
|
|
26
|
+
single static binary the installer can fetch.
|
|
27
|
+
|
|
28
|
+
### Why no Beancount in v0?
|
|
29
|
+
|
|
30
|
+
Beancount makes the project look like an accounting tool, not a freelancer
|
|
31
|
+
tool. v0's audience is "freelancer who wants to log time and send invoices."
|
|
32
|
+
The double-entry ledger lands in v1 alongside expenses, which is when
|
|
33
|
+
double-entry actually pulls its weight.
|
|
34
|
+
|
|
35
|
+
## Agent loop
|
|
36
|
+
|
|
37
|
+
Single-turn tool-using loop, not multi-turn autonomy in v0.
|
|
38
|
+
|
|
39
|
+
A user message comes in. Claude is given the system prompt + the message +
|
|
40
|
+
the current tools. Claude either responds with text or proposes one or more
|
|
41
|
+
tool calls. Tool calls are executed (with approval for writes). The result
|
|
42
|
+
is fed back to Claude, which either calls more tools or produces a final
|
|
43
|
+
text response. Then we wait for the next user message.
|
|
44
|
+
|
|
45
|
+
We are explicitly NOT building an autonomous loop in v0. Every user
|
|
46
|
+
interaction is initiated by the user. No background work. No daemons.
|
|
47
|
+
|
|
48
|
+
### Tools exposed to Claude
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
read_text(path: str) -> str
|
|
52
|
+
Read any file under the project root. No approval needed.
|
|
53
|
+
|
|
54
|
+
list_clients() -> list[Client]
|
|
55
|
+
Read clients.toml and return parsed entries. No approval needed.
|
|
56
|
+
|
|
57
|
+
list_projects() -> list[Project]
|
|
58
|
+
Read projects.toml and return parsed entries. No approval needed.
|
|
59
|
+
|
|
60
|
+
run_hledger(args: list[str]) -> str
|
|
61
|
+
Run hledger with the project's time.timeclock as -f. Read-only.
|
|
62
|
+
|
|
63
|
+
append_timeclock(entries: list[TimeclockEntry]) -> None
|
|
64
|
+
APPROVAL REQUIRED. Append timeclock entries to time.timeclock.
|
|
65
|
+
|
|
66
|
+
render_invoice(client_slug: str, period: Period, line_items: list[LineItem]) -> Path
|
|
67
|
+
APPROVAL REQUIRED. Generate the invoice .md and .pdf, increment counter.
|
|
68
|
+
|
|
69
|
+
upsert_client(client: Client) -> None
|
|
70
|
+
APPROVAL REQUIRED. Add or update a client in clients.toml.
|
|
71
|
+
|
|
72
|
+
upsert_project(project: Project) -> None
|
|
73
|
+
APPROVAL REQUIRED. Add or update a project in projects.toml.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Approval UX
|
|
77
|
+
|
|
78
|
+
When Claude proposes a write, the CLI:
|
|
79
|
+
|
|
80
|
+
1. Prints the human-readable diff (Rich-rendered).
|
|
81
|
+
2. Prompts: `Apply? [y/N/edit]` — `edit` opens `$EDITOR` on a temp file
|
|
82
|
+
containing the proposed change so the user can tweak before applying.
|
|
83
|
+
3. On `y`, applies the change and returns success to the agent.
|
|
84
|
+
4. On `N`, returns a tool error like "User declined the change" so the
|
|
85
|
+
agent can react conversationally.
|
|
86
|
+
|
|
87
|
+
## System prompt
|
|
88
|
+
|
|
89
|
+
Loaded from `prompts/system.md`, version-controlled in the repo. It
|
|
90
|
+
establishes:
|
|
91
|
+
|
|
92
|
+
- Halyard's role and constraints.
|
|
93
|
+
- The file layout and formats it can rely on.
|
|
94
|
+
- Formatting rules for timeclock entries.
|
|
95
|
+
- Escalation behavior (when in doubt, ask; never invent a slug).
|
|
96
|
+
- Tone (concise, direct, terminal-native).
|
|
97
|
+
|
|
98
|
+
## Out-of-scope clarification
|
|
99
|
+
|
|
100
|
+
No async, no daemons, no LSP, no extension points in v0. Each command is a
|
|
101
|
+
fresh process; state lives entirely in files. The only persistent runtime
|
|
102
|
+
state is `~/.halyard/active` (the active timer), which is a single line of
|
|
103
|
+
text.
|
|
104
|
+
|
|
105
|
+
## Testing strategy
|
|
106
|
+
|
|
107
|
+
- Unit tests for parsers (timeclock round-trip, TOML schema validation).
|
|
108
|
+
- Golden-file tests for the default invoice template — render a fixed input
|
|
109
|
+
and diff against a checked-in expected output.
|
|
110
|
+
- A small integration test using Anthropic's prompt-caching test endpoint
|
|
111
|
+
is **out of scope** for v0; we mock the agent layer in unit tests and
|
|
112
|
+
exercise the real model only manually until v1.
|
|
113
|
+
|
|
114
|
+
## Open questions (resolve before implementation)
|
|
115
|
+
|
|
116
|
+
1. Default currency formatting: rely on Babel, or hard-code USD/EUR/GBP for v0?
|
|
117
|
+
2. typst install: bundle as a Python package dep, ship a small downloader
|
|
118
|
+
script in `halyard init`, or instruct users to install separately?
|
|
119
|
+
3. API key handling: env var only (`ANTHROPIC_API_KEY`), or also a
|
|
120
|
+
`~/.halyard/config.toml` entry? Env var only is simpler and safer.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# v0: Time tracking and invoice drafting
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Freelancers waste hours every month on the same loop: scattered time notes,
|
|
6
|
+
chasing invoice templates, fighting accounting software they hate. Existing
|
|
7
|
+
tools (FreshBooks, QuickBooks, Harvest) are SaaS, lock data in proprietary
|
|
8
|
+
databases, and have nothing meaningful in the way of an AI agent.
|
|
9
|
+
|
|
10
|
+
We're building the smallest version of Halyard that can demo the full thesis
|
|
11
|
+
in 60 seconds: log time in natural language, generate an invoice from those
|
|
12
|
+
hours, and prove the data is just plain text the user owns.
|
|
13
|
+
|
|
14
|
+
The deliverable for v0 is *the demo video*, not the binary. The binary exists
|
|
15
|
+
to make the video true.
|
|
16
|
+
|
|
17
|
+
## What's changing
|
|
18
|
+
|
|
19
|
+
- New CLI binary `halyard` with `init`, `log`, `start`, `stop`, `invoice`,
|
|
20
|
+
and an interactive REPL that drops you into a Claude-powered session.
|
|
21
|
+
- New project layout: `halyard.toml`, `clients.toml`, `projects.toml`,
|
|
22
|
+
`time.timeclock`, `invoices/`.
|
|
23
|
+
- Default markdown invoice template + typst PDF renderer.
|
|
24
|
+
- Single-turn agent loop with three tools: `read_text`, `append_timeclock`,
|
|
25
|
+
`render_invoice`. Plus `run_hledger` for read-only queries. All writes
|
|
26
|
+
require user approval.
|
|
27
|
+
|
|
28
|
+
## Out of scope (v0)
|
|
29
|
+
|
|
30
|
+
- Expenses, bank sync, reconciliation → v1
|
|
31
|
+
- Local web UI → v1
|
|
32
|
+
- Plugin/skill system → v1
|
|
33
|
+
- Beancount ledger → v1
|
|
34
|
+
- Tax forms, e-file → v3+
|
|
35
|
+
- Multi-currency edge cases → v2
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# CLI Spec
|
|
2
|
+
|
|
3
|
+
## Requirement: halyard init
|
|
4
|
+
|
|
5
|
+
The CLI MUST scaffold a new Halyard project in the current directory.
|
|
6
|
+
|
|
7
|
+
### Scenario: first-time setup
|
|
8
|
+
|
|
9
|
+
- WHEN the user runs `halyard init` in an empty directory
|
|
10
|
+
- THEN the CLI creates `halyard.toml`, `clients.toml`, `projects.toml`,
|
|
11
|
+
`time.timeclock`, `invoices/`, and `.gitignore`
|
|
12
|
+
- AND prints a welcome message with three suggested next commands
|
|
13
|
+
|
|
14
|
+
### Scenario: existing Halyard project
|
|
15
|
+
|
|
16
|
+
- WHEN the user runs `halyard init` in a directory containing `halyard.toml`
|
|
17
|
+
- THEN the CLI exits with code 1
|
|
18
|
+
- AND prints a message instructing the user to remove or move the existing
|
|
19
|
+
project before re-initializing
|
|
20
|
+
|
|
21
|
+
## Requirement: halyard log (natural language)
|
|
22
|
+
|
|
23
|
+
The CLI MUST accept a free-form string and use Claude to extract a
|
|
24
|
+
timeclock entry.
|
|
25
|
+
|
|
26
|
+
### Scenario: simple past-tense log
|
|
27
|
+
|
|
28
|
+
- WHEN the user runs `halyard log "worked 3h on ACME auth migration this morning"`
|
|
29
|
+
- THEN Claude extracts client=acme, project=auth-migration, duration=3h,
|
|
30
|
+
start=this morning (resolved against the current local date)
|
|
31
|
+
- AND the CLI displays the proposed timeclock lines and prompts for confirmation
|
|
32
|
+
- AND on confirmation, appends them to `time.timeclock`
|
|
33
|
+
|
|
34
|
+
### Scenario: ambiguous client
|
|
35
|
+
|
|
36
|
+
- WHEN the user logs time against a client name not present in `clients.toml`
|
|
37
|
+
- THEN Claude proposes adding the client
|
|
38
|
+
- AND asks for the hourly rate before logging the time entry
|
|
39
|
+
|
|
40
|
+
### Scenario: declined confirmation
|
|
41
|
+
|
|
42
|
+
- WHEN the user is prompted to confirm a proposed entry and declines
|
|
43
|
+
- THEN no files are written
|
|
44
|
+
- AND the CLI exits cleanly with code 0
|
|
45
|
+
|
|
46
|
+
## Requirement: halyard start / stop
|
|
47
|
+
|
|
48
|
+
The CLI MUST support live timing.
|
|
49
|
+
|
|
50
|
+
### Scenario: start a timer
|
|
51
|
+
|
|
52
|
+
- WHEN the user runs `halyard start acme/auth-migration`
|
|
53
|
+
- THEN the CLI writes an `i` line to `time.timeclock` with the current
|
|
54
|
+
timestamp and the slug `acme:auth-migration`
|
|
55
|
+
- AND records the active timer in `~/.halyard/active`
|
|
56
|
+
|
|
57
|
+
### Scenario: stop the active timer
|
|
58
|
+
|
|
59
|
+
- WHEN the user runs `halyard stop` and a timer is active
|
|
60
|
+
- THEN the CLI writes an `o` line to `time.timeclock` with the current timestamp
|
|
61
|
+
- AND clears the active timer file
|
|
62
|
+
|
|
63
|
+
### Scenario: stop with no active timer
|
|
64
|
+
|
|
65
|
+
- WHEN the user runs `halyard stop` and no timer is active
|
|
66
|
+
- THEN the CLI exits with code 1 and a clear message
|
|
67
|
+
|
|
68
|
+
## Requirement: halyard invoice
|
|
69
|
+
|
|
70
|
+
The CLI MUST generate invoices from time entries for a client and date range.
|
|
71
|
+
|
|
72
|
+
### Scenario: invoice last month for a client
|
|
73
|
+
|
|
74
|
+
- WHEN the user runs `halyard invoice acme --month last`
|
|
75
|
+
- THEN the CLI reads `time.timeclock`, sums hours by project for ACME in
|
|
76
|
+
the prior calendar month
|
|
77
|
+
- AND applies hourly rates from `clients.toml` (with project-level overrides
|
|
78
|
+
from `projects.toml` taking precedence)
|
|
79
|
+
- AND generates `invoices/2026-04-acme-001.md` and the corresponding `.pdf`
|
|
80
|
+
- AND increments the invoice counter in `halyard.toml`
|
|
81
|
+
- AND opens the PDF using the platform-default viewer
|
|
82
|
+
|
|
83
|
+
### Scenario: no time logged
|
|
84
|
+
|
|
85
|
+
- WHEN there are zero entries in the requested range for the requested client
|
|
86
|
+
- THEN the CLI exits with code 1 and a clear message
|
|
87
|
+
- AND no files are written
|
|
88
|
+
|
|
89
|
+
### Scenario: explicit date range
|
|
90
|
+
|
|
91
|
+
- WHEN the user runs `halyard invoice acme --from 2026-04-01 --to 2026-04-15`
|
|
92
|
+
- THEN the same logic as `--month` applies, scoped to that range
|
|
93
|
+
|
|
94
|
+
## Requirement: halyard (bare command, REPL mode)
|
|
95
|
+
|
|
96
|
+
The CLI MUST drop into an interactive Claude session when invoked with no
|
|
97
|
+
subcommand.
|
|
98
|
+
|
|
99
|
+
### Scenario: conversational logging
|
|
100
|
+
|
|
101
|
+
- WHEN the user runs `halyard`
|
|
102
|
+
- AND types "I just finished 2 hours on Globex"
|
|
103
|
+
- THEN the agent proposes the timeclock entry, the user confirms,
|
|
104
|
+
the file is appended, and the agent waits for the next message
|
|
105
|
+
|
|
106
|
+
### Scenario: query
|
|
107
|
+
|
|
108
|
+
- WHEN the user types "how many hours have I billed ACME this quarter"
|
|
109
|
+
- THEN the agent runs the equivalent `hledger -f time.timeclock` query
|
|
110
|
+
- AND returns a table or short prose summary
|
|
111
|
+
- AND no files are modified
|
|
112
|
+
|
|
113
|
+
### Scenario: graceful exit
|
|
114
|
+
|
|
115
|
+
- WHEN the user types `/quit` or sends EOF (Ctrl-D)
|
|
116
|
+
- THEN the REPL exits cleanly with code 0
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Data Model Spec
|
|
2
|
+
|
|
3
|
+
## Requirement: project layout
|
|
4
|
+
|
|
5
|
+
A Halyard project is a directory containing exactly these files at the root
|
|
6
|
+
after `halyard init`:
|
|
7
|
+
|
|
8
|
+
- `halyard.toml` — project config (business name, default currency,
|
|
9
|
+
invoice counter, default invoice due-window in days)
|
|
10
|
+
- `clients.toml` — array of clients
|
|
11
|
+
- `projects.toml` — array of projects
|
|
12
|
+
- `time.timeclock` — append-only hledger timeclock file
|
|
13
|
+
- `invoices/` — directory of generated invoice `.md` and `.pdf` files
|
|
14
|
+
- `.gitignore` — excludes `*.pdf` if the user opts in, plus
|
|
15
|
+
`.halyard-cache/` and `~/.halyard/`
|
|
16
|
+
|
|
17
|
+
## Requirement: halyard.toml schema
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[business]
|
|
21
|
+
name = "M. Camaj Consulting"
|
|
22
|
+
currency = "USD"
|
|
23
|
+
default_due_days = 30
|
|
24
|
+
|
|
25
|
+
[invoicing]
|
|
26
|
+
counter = 0 # next invoice number suffix
|
|
27
|
+
prefix = "{year}-{month:02d}-{client_slug}"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Requirement: clients.toml schema
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[[client]]
|
|
34
|
+
slug = "acme" # required, lowercase, [a-z0-9-]
|
|
35
|
+
name = "Acme Corp" # required
|
|
36
|
+
hourly_rate = 150 # required, numeric
|
|
37
|
+
email = "ap@acme.com" # optional
|
|
38
|
+
address = """ # optional, multi-line ok
|
|
39
|
+
123 Main St
|
|
40
|
+
Anytown, ST 12345
|
|
41
|
+
"""
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Requirement: projects.toml schema
|
|
45
|
+
|
|
46
|
+
```toml
|
|
47
|
+
[[project]]
|
|
48
|
+
slug = "auth-migration" # required, scoped under client_slug
|
|
49
|
+
client_slug = "acme" # required, must match a client
|
|
50
|
+
name = "Auth migration" # required
|
|
51
|
+
hourly_rate = 175 # optional override; falls back to client rate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Requirement: timeclock format
|
|
55
|
+
|
|
56
|
+
Time entries MUST conform to hledger timeclock format:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
i YYYY-MM-DD HH:MM:SS <client-slug>:<project-slug> optional comment
|
|
60
|
+
o YYYY-MM-DD HH:MM:SS
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The file is parseable by `hledger` directly, with no Halyard-specific
|
|
64
|
+
extension. Compatibility with the broader plaintext-accounting ecosystem
|
|
65
|
+
is a hard requirement — users SHOULD be able to drop Fava or hledger-web
|
|
66
|
+
on top of `time.timeclock` and have it just work.
|
|
67
|
+
|
|
68
|
+
## Requirement: invoice format
|
|
69
|
+
|
|
70
|
+
Each invoice is a markdown file with YAML frontmatter:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
---
|
|
74
|
+
invoice_number: 2026-04-acme-001
|
|
75
|
+
client_slug: acme
|
|
76
|
+
issue_date: 2026-04-30
|
|
77
|
+
due_date: 2026-05-30
|
|
78
|
+
currency: USD
|
|
79
|
+
line_items:
|
|
80
|
+
- description: Auth migration
|
|
81
|
+
project_slug: auth-migration
|
|
82
|
+
hours: 12.5
|
|
83
|
+
rate: 175
|
|
84
|
+
amount: 2187.50
|
|
85
|
+
total: 2187.50
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
# Invoice 2026-04-acme-001
|
|
89
|
+
|
|
90
|
+
(rendered body — generated from the template)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The body is rendered from `templates/invoice.md.j2` if present in the
|
|
94
|
+
project, otherwise from the built-in default template shipped with Halyard.
|
|
95
|
+
|
|
96
|
+
## Requirement: invoice number sequencing
|
|
97
|
+
|
|
98
|
+
Invoice numbers are sequential within a `(year, month, client)` tuple. The
|
|
99
|
+
counter in `halyard.toml` is global; the prefix template controls how that
|
|
100
|
+
counter renders into a final number.
|
|
101
|
+
|
|
102
|
+
### Scenario: first invoice of the month for a client
|
|
103
|
+
|
|
104
|
+
- WHEN the project has no prior invoice for ACME in 2026-04
|
|
105
|
+
- THEN the next invoice for ACME in that month is `2026-04-acme-001`
|
|
106
|
+
|
|
107
|
+
### Scenario: second invoice of the month for the same client
|
|
108
|
+
|
|
109
|
+
- WHEN the project already has `2026-04-acme-001`
|
|
110
|
+
- THEN the next is `2026-04-acme-002`
|
|
111
|
+
|
|
112
|
+
### Scenario: different clients, same month
|
|
113
|
+
|
|
114
|
+
- WHEN the project has `2026-04-acme-001` and the user invoices Globex
|
|
115
|
+
- THEN the next is `2026-04-globex-001`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Tasks
|
|
2
|
+
|
|
3
|
+
The implementation checklist for v0. Task IDs are referenced from the CLI
|
|
4
|
+
stub via `NotImplementedError("v0 task X.Y")` markers — when you implement a
|
|
5
|
+
task, update both the code and tick the box here.
|
|
6
|
+
|
|
7
|
+
## 1. Project skeleton
|
|
8
|
+
|
|
9
|
+
- [x] 1.1 Initialize Python package, pyproject.toml, ruff config
|
|
10
|
+
- [x] 1.2 Set up Typer CLI entry point with stub commands
|
|
11
|
+
- [ ] 1.3 Add CI: GitHub Actions running ruff + mypy + pytest on pushes/PRs
|
|
12
|
+
- [ ] 1.4 Reserve `halyard` on PyPI (publish a 0.0.1 placeholder)
|
|
13
|
+
- [ ] 1.5 Reserve a domain (halyard.dev or similar)
|
|
14
|
+
|
|
15
|
+
## 2. Data model
|
|
16
|
+
|
|
17
|
+
- [ ] 2.1 Pydantic models: `HalyardConfig`, `Client`, `Project`, `LineItem`,
|
|
18
|
+
`TimeclockEntry`, `Invoice`
|
|
19
|
+
- [ ] 2.2 TOML readers/writers for `halyard.toml`, `clients.toml`,
|
|
20
|
+
`projects.toml` (round-trip safe; preserves comments where possible)
|
|
21
|
+
- [ ] 2.3 Implement `halyard init` — creates the full project layout from
|
|
22
|
+
sensible defaults
|
|
23
|
+
|
|
24
|
+
## 3. Time tracking
|
|
25
|
+
|
|
26
|
+
- [ ] 3.1 Implement `halyard start <slug>` and `halyard stop`, including the
|
|
27
|
+
`~/.halyard/active` state file
|
|
28
|
+
- [ ] 3.2 Implement `halyard log <text>`: send to Claude, render the
|
|
29
|
+
proposal, prompt for approval, append on confirm
|
|
30
|
+
- [ ] 3.3 dateparser integration for "this morning", "yesterday", "last
|
|
31
|
+
Tuesday", etc., with a fixed reference timezone (the user's local)
|
|
32
|
+
- [ ] 3.4 Timeclock parser/writer with round-trip safety (formatting,
|
|
33
|
+
comments)
|
|
34
|
+
|
|
35
|
+
## 4. Invoicing
|
|
36
|
+
|
|
37
|
+
- [ ] 4.1 Default Jinja invoice template — clean, professional, renders
|
|
38
|
+
well at letter and A4
|
|
39
|
+
- [ ] 4.2 Implement `halyard invoice <client>` with `--month`, `--from`,
|
|
40
|
+
`--to` flags
|
|
41
|
+
- [ ] 4.3 typst PDF rendering pipeline (subprocess; verify install on first
|
|
42
|
+
run with a friendly error if missing)
|
|
43
|
+
- [ ] 4.4 Invoice number sequencing in `halyard.toml` per the spec scenarios
|
|
44
|
+
- [ ] 4.5 Open the PDF after generation using the platform-default viewer
|
|
45
|
+
|
|
46
|
+
## 5. Agent loop
|
|
47
|
+
|
|
48
|
+
- [ ] 5.1 Anthropic SDK integration + tool definitions (read_text,
|
|
49
|
+
list_clients, list_projects, run_hledger, append_timeclock,
|
|
50
|
+
render_invoice, upsert_client, upsert_project)
|
|
51
|
+
- [ ] 5.2 First version of `prompts/system.md`
|
|
52
|
+
- [ ] 5.3 Approval prompt UX (Rich-based diff renderer, y/N/edit flow)
|
|
53
|
+
- [ ] 5.4 Implement `halyard` (no args) REPL mode — readline history,
|
|
54
|
+
slash commands (`/quit`, `/help`, `/model`)
|
|
55
|
+
|
|
56
|
+
## 6. Demo + launch
|
|
57
|
+
|
|
58
|
+
- [ ] 6.1 Record the 60-second demo video (this is the actual deliverable
|
|
59
|
+
for v0 — the binary exists to make the video true)
|
|
60
|
+
- [ ] 6.2 README polish: animated GIF, install instructions, the pitch
|
|
61
|
+
- [ ] 6.3 Draft Show HN, Lobsters, /r/plaintextaccounting posts; sit on
|
|
62
|
+
them until the demo is good
|
|
63
|
+
- [ ] 6.4 Tag and publish v0.1.0 on PyPI
|
|
64
|
+
- [ ] 6.5 Cross-post the demo video to X with the project pitch line
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Halyard — Project Context
|
|
2
|
+
|
|
3
|
+
This file is loaded by OpenSpec on every change. It establishes shared
|
|
4
|
+
conventions so each change proposal doesn't have to re-explain them.
|
|
5
|
+
|
|
6
|
+
## Mission
|
|
7
|
+
|
|
8
|
+
Halyard is a plain-text, agent-native financial OS for freelancers and
|
|
9
|
+
one-person businesses. Time, expenses, clients, projects, and invoices live
|
|
10
|
+
as plain-text files on the user's machine. An AI agent (Claude) reads and
|
|
11
|
+
writes those files on the user's behalf.
|
|
12
|
+
|
|
13
|
+
## Non-negotiables
|
|
14
|
+
|
|
15
|
+
These constraints apply to every change. Any proposal that breaks them needs
|
|
16
|
+
to explicitly justify the exception.
|
|
17
|
+
|
|
18
|
+
1. **Local-first.** No required cloud service. Optional paid tiers may exist
|
|
19
|
+
later for sync or e-filing, but the core product runs offline against a
|
|
20
|
+
local folder.
|
|
21
|
+
2. **Plain text forever.** All user data is stored in human-readable,
|
|
22
|
+
diff-friendly text formats with public specs:
|
|
23
|
+
- Time → [hledger timeclock](https://hledger.org/timeclock.html)
|
|
24
|
+
- Ledger → [Beancount](https://beancount.github.io/)
|
|
25
|
+
- Invoices → markdown with YAML frontmatter
|
|
26
|
+
- Config → TOML
|
|
27
|
+
No proprietary formats. No SQLite-as-source-of-truth.
|
|
28
|
+
3. **Files are the source of truth.** Any UI (CLI, web, future GUI) is a
|
|
29
|
+
view onto the files. The agent edits the same files a human would.
|
|
30
|
+
4. **No silent writes.** Any modification to user data is proposed to the
|
|
31
|
+
user with a diff and waits for approval. Read-only operations need no
|
|
32
|
+
approval.
|
|
33
|
+
5. **MIT licensed.** Permissively. Forever.
|
|
34
|
+
|
|
35
|
+
## Project layout (per Halyard project)
|
|
36
|
+
|
|
37
|
+
A user's Halyard project directory contains:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
my-business/
|
|
41
|
+
├── halyard.toml # business name, currency, invoice counter
|
|
42
|
+
├── clients.toml # array of clients
|
|
43
|
+
├── projects.toml # array of projects (linked to client_slug)
|
|
44
|
+
├── time.timeclock # hledger timeclock format
|
|
45
|
+
├── ledger.beancount # Beancount ledger (added in v1)
|
|
46
|
+
├── invoices/ # generated invoice .md and .pdf files
|
|
47
|
+
├── expenses/ # raw bank/receipt CSVs (added in v1)
|
|
48
|
+
├── templates/ # optional user overrides
|
|
49
|
+
│ └── invoice.md.j2
|
|
50
|
+
└── .gitignore
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Per-user agent state (skills, API keys, active timer) lives in
|
|
54
|
+
`~/.halyard/`, not in the project folder.
|
|
55
|
+
|
|
56
|
+
## Stack defaults
|
|
57
|
+
|
|
58
|
+
- **Language:** Python 3.11+
|
|
59
|
+
- **CLI:** Typer + Rich
|
|
60
|
+
- **Models:** Pydantic v2
|
|
61
|
+
- **Templating:** Jinja2
|
|
62
|
+
- **PDF:** typst (subprocess)
|
|
63
|
+
- **Time parsing:** dateparser
|
|
64
|
+
- **Agent:** Anthropic SDK with tool use (single-turn loop in v0)
|
|
65
|
+
- **Tests:** pytest with golden-file tests for renders
|
|
66
|
+
- **Lint/format:** ruff
|
|
67
|
+
|
|
68
|
+
Any deviation from this stack needs justification in the change's `design.md`.
|
|
69
|
+
|
|
70
|
+
## How changes work
|
|
71
|
+
|
|
72
|
+
Each change lives at `openspec/changes/<change-slug>/` with:
|
|
73
|
+
|
|
74
|
+
- `proposal.md` — why & what's changing (high level)
|
|
75
|
+
- `specs/*.md` — requirements with scenarios, in WHEN/THEN form
|
|
76
|
+
- `design.md` — technical approach, choices, trade-offs
|
|
77
|
+
- `tasks.md` — the implementation checklist
|
|
78
|
+
|
|
79
|
+
Completed changes get archived to `openspec/changes/archive/YYYY-MM-DD-<slug>/`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Halyard Agent — System Prompt (v0)
|
|
2
|
+
|
|
3
|
+
You are Halyard, a plain-text, agent-native financial assistant for
|
|
4
|
+
freelancers and one-person businesses. You help the user log time, manage
|
|
5
|
+
clients and projects, and draft invoices. You operate against a known
|
|
6
|
+
directory layout and well-defined file formats.
|
|
7
|
+
|
|
8
|
+
## What you can rely on
|
|
9
|
+
|
|
10
|
+
The user is in a Halyard project directory containing:
|
|
11
|
+
|
|
12
|
+
- `halyard.toml` — project config (business name, default currency,
|
|
13
|
+
invoice counter)
|
|
14
|
+
- `clients.toml` — clients with name, slug, hourly_rate, address, email
|
|
15
|
+
- `projects.toml` — projects with slug, client_slug, name, optional rate
|
|
16
|
+
override
|
|
17
|
+
- `time.timeclock` — append-only hledger timeclock file
|
|
18
|
+
- `invoices/` — generated invoice markdown and PDF files
|
|
19
|
+
|
|
20
|
+
If any of these are missing, ask the user to run `halyard init`. Do not
|
|
21
|
+
attempt to create them yourself.
|
|
22
|
+
|
|
23
|
+
## Time entries
|
|
24
|
+
|
|
25
|
+
When the user describes work in natural language ("I just finished 2h on
|
|
26
|
+
ACME's auth migration"), produce hledger timeclock entries:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
i 2026-05-06 09:00:00 acme:auth-migration Auth migration session
|
|
30
|
+
o 2026-05-06 11:00:00
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Default rules when information is missing:
|
|
34
|
+
|
|
35
|
+
- If the user says "this morning," "yesterday afternoon," etc., resolve
|
|
36
|
+
against the current local date and time.
|
|
37
|
+
- If the user gives only duration ("worked 2h on ACME"), use the most
|
|
38
|
+
recent stop time as the start. If there is no recent activity, use
|
|
39
|
+
current time minus the duration.
|
|
40
|
+
- If the client or project slug is unknown, **do not invent it.** Propose
|
|
41
|
+
adding it via `upsert_client` / `upsert_project` first, asking for any
|
|
42
|
+
required fields you don't know (hourly_rate, full name).
|
|
43
|
+
- The slug format is `client_slug:project_slug`, lowercase, hyphenated.
|
|
44
|
+
|
|
45
|
+
## Invoices
|
|
46
|
+
|
|
47
|
+
When asked to draft an invoice, read `time.timeclock`, sum hours by project
|
|
48
|
+
for the requested client and date range, multiply by the appropriate rate
|
|
49
|
+
(project-level override beats client-level rate), and produce line items.
|
|
50
|
+
Use the template at `templates/invoice.md.j2` if the user has one,
|
|
51
|
+
otherwise the built-in default. Increment the invoice counter in
|
|
52
|
+
`halyard.toml`.
|
|
53
|
+
|
|
54
|
+
If there are zero hours in the range, do not create an invoice. Tell the
|
|
55
|
+
user.
|
|
56
|
+
|
|
57
|
+
## Approval
|
|
58
|
+
|
|
59
|
+
You never silently modify files. Every write to `time.timeclock`,
|
|
60
|
+
`halyard.toml`, `clients.toml`, `projects.toml`, or anything in `invoices/`
|
|
61
|
+
goes through a tool call that prompts the user for approval and shows a
|
|
62
|
+
diff. Read-only operations (queries, summaries, hledger reports) need no
|
|
63
|
+
approval.
|
|
64
|
+
|
|
65
|
+
If the user declines a proposed change, do not retry the same change. Ask
|
|
66
|
+
what they'd like to adjust.
|
|
67
|
+
|
|
68
|
+
## Tone
|
|
69
|
+
|
|
70
|
+
Concise. Direct. Terminal-native. Show, don't narrate — when you write a
|
|
71
|
+
timeclock entry, just show the lines being added. When you generate an
|
|
72
|
+
invoice, show the totals. Don't pad with phrases like "Sure, I'd be happy
|
|
73
|
+
to help!" or "Let me think about that..."
|
|
74
|
+
|
|
75
|
+
The user is a freelancer in a terminal who wants to get back to work. Save
|
|
76
|
+
them keystrokes.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "halyard"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Plain-text, agent-native financial OS for freelancers."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Kormilo LLC" }]
|
|
9
|
+
keywords = ["freelance", "invoicing", "time-tracking", "accounting", "plaintext", "agent", "claude"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: End Users/Desktop",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer>=0.12",
|
|
21
|
+
"rich>=13.7",
|
|
22
|
+
"pydantic>=2.6",
|
|
23
|
+
"tomli>=2.0; python_version<'3.11'",
|
|
24
|
+
"tomli-w>=1.0",
|
|
25
|
+
"jinja2>=3.1",
|
|
26
|
+
"dateparser>=1.2",
|
|
27
|
+
"anthropic>=0.40",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
"pytest-cov>=5.0",
|
|
34
|
+
"ruff>=0.5",
|
|
35
|
+
"mypy>=1.10",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
halyard = "halyard.cli:app"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/Kormiloio/Halyard"
|
|
43
|
+
Repository = "https://github.com/Kormiloio/Halyard"
|
|
44
|
+
Issues = "https://github.com/Kormiloio/Halyard/issues"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/halyard"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = "py311"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "I", "N", "B", "UP", "C4", "SIM", "RUF"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint.per-file-ignores]
|
|
61
|
+
"tests/*" = ["S101"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
addopts = "-ra -q"
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.11"
|
|
69
|
+
strict = true
|
|
70
|
+
warn_return_any = true
|
|
71
|
+
warn_unused_configs = true
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Halyard CLI entry point.
|
|
2
|
+
|
|
3
|
+
This module defines the command surface for v0. Each command currently raises
|
|
4
|
+
NotImplementedError pointing at the task in
|
|
5
|
+
`openspec/changes/v0-time-and-invoice/tasks.md` that will fill it in.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="halyard",
|
|
14
|
+
help=(
|
|
15
|
+
"Plain-text, agent-native financial OS. "
|
|
16
|
+
"Your books in plain text. Owned by you. Operated by Claude."
|
|
17
|
+
),
|
|
18
|
+
no_args_is_help=False,
|
|
19
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
20
|
+
)
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback(invoke_without_command=True)
|
|
25
|
+
def default(ctx: typer.Context) -> None:
|
|
26
|
+
"""Drop into the interactive Claude REPL when invoked with no subcommand."""
|
|
27
|
+
if ctx.invoked_subcommand is None:
|
|
28
|
+
# TODO(v0 task 5.4): launch interactive agent REPL.
|
|
29
|
+
console.print(
|
|
30
|
+
"[bold cyan]Halyard[/] — interactive REPL not yet implemented "
|
|
31
|
+
"(see openspec/changes/v0-time-and-invoice/tasks.md task 5.4)."
|
|
32
|
+
)
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def init() -> None:
|
|
38
|
+
"""Scaffold a new Halyard project in the current directory."""
|
|
39
|
+
raise NotImplementedError("v0 task 2.3")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def log(
|
|
44
|
+
message: str = typer.Argument(..., help="Natural-language description of work."),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Log time from a free-form description (calls Claude to extract the entry)."""
|
|
47
|
+
raise NotImplementedError("v0 task 3.2")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def start(
|
|
52
|
+
slug: str = typer.Argument(..., help="client/project slug, e.g. acme/auth-migration"),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Start the active timer."""
|
|
55
|
+
raise NotImplementedError("v0 task 3.1")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command()
|
|
59
|
+
def stop() -> None:
|
|
60
|
+
"""Stop the active timer."""
|
|
61
|
+
raise NotImplementedError("v0 task 3.1")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def invoice(
|
|
66
|
+
client: str = typer.Argument(..., help="Client slug to invoice."),
|
|
67
|
+
month: str | None = typer.Option(
|
|
68
|
+
None, "--month", help="last | this | YYYY-MM"
|
|
69
|
+
),
|
|
70
|
+
from_: str | None = typer.Option(
|
|
71
|
+
None, "--from", help="ISO date (inclusive lower bound)"
|
|
72
|
+
),
|
|
73
|
+
to: str | None = typer.Option(
|
|
74
|
+
None, "--to", help="ISO date (inclusive upper bound)"
|
|
75
|
+
),
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Generate an invoice from logged time entries."""
|
|
78
|
+
raise NotImplementedError("v0 task 4.2")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
app()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{# Default Halyard invoice template.
|
|
2
|
+
Rendered to markdown, then converted to PDF via typst.
|
|
3
|
+
Users can override this by creating templates/invoice.md.j2 in their
|
|
4
|
+
project directory. -#}
|
|
5
|
+
---
|
|
6
|
+
invoice_number: {{ invoice.invoice_number }}
|
|
7
|
+
client_slug: {{ invoice.client_slug }}
|
|
8
|
+
issue_date: {{ invoice.issue_date }}
|
|
9
|
+
due_date: {{ invoice.due_date }}
|
|
10
|
+
currency: {{ invoice.currency }}
|
|
11
|
+
total: {{ "%.2f"|format(invoice.total) }}
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Invoice {{ invoice.invoice_number }}
|
|
15
|
+
|
|
16
|
+
**{{ business.name }}**
|
|
17
|
+
Issued: {{ invoice.issue_date }}
|
|
18
|
+
Due: {{ invoice.due_date }}
|
|
19
|
+
|
|
20
|
+
## Bill to
|
|
21
|
+
|
|
22
|
+
**{{ client.name }}**
|
|
23
|
+
{%- if client.address %}
|
|
24
|
+
{{ client.address }}
|
|
25
|
+
{%- endif %}
|
|
26
|
+
{%- if client.email %}
|
|
27
|
+
{{ client.email }}
|
|
28
|
+
{%- endif %}
|
|
29
|
+
|
|
30
|
+
## Line items
|
|
31
|
+
|
|
32
|
+
| Description | Hours | Rate | Amount |
|
|
33
|
+
|---|---:|---:|---:|
|
|
34
|
+
{%- for item in invoice.line_items %}
|
|
35
|
+
| {{ item.description }} | {{ "%.2f"|format(item.hours) }} | {{ invoice.currency }} {{ "%.2f"|format(item.rate) }} | {{ invoice.currency }} {{ "%.2f"|format(item.amount) }} |
|
|
36
|
+
{%- endfor %}
|
|
37
|
+
|
|
38
|
+
**Total: {{ invoice.currency }} {{ "%.2f"|format(invoice.total) }}**
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Payable within {{ (invoice.due_date - invoice.issue_date).days }} days.
|
|
43
|
+
Thank you for your business.
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Smoke tests for the CLI surface.
|
|
2
|
+
|
|
3
|
+
These do not exercise behavior — that's what the v0 task tests will do. They
|
|
4
|
+
just guarantee that the CLI app constructs and exposes the expected commands.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from halyard.cli import app
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_help_runs() -> None:
|
|
16
|
+
result = runner.invoke(app, ["--help"])
|
|
17
|
+
assert result.exit_code == 0
|
|
18
|
+
assert "Halyard" in result.stdout or "halyard" in result.stdout
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_v0_commands_registered() -> None:
|
|
22
|
+
result = runner.invoke(app, ["--help"])
|
|
23
|
+
for cmd in ("init", "log", "start", "stop", "invoice"):
|
|
24
|
+
assert cmd in result.stdout, f"command {cmd!r} not registered"
|