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.
@@ -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
@@ -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.
@@ -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,3 @@
1
+ """Halyard: plain-text, agent-native financial OS for freelancers."""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,4 @@
1
+ from halyard.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -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"