damien 0.4.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.
- damien-0.4.1/.env.example +37 -0
- damien-0.4.1/.github/workflows/release.yml +42 -0
- damien-0.4.1/.github/workflows/test.yml +48 -0
- damien-0.4.1/.gitignore +19 -0
- damien-0.4.1/LICENSE +21 -0
- damien-0.4.1/PKG-INFO +342 -0
- damien-0.4.1/README.md +305 -0
- damien-0.4.1/docs/AUDIT-BRIEF.md +92 -0
- damien-0.4.1/docs/COMMANDS.md +202 -0
- damien-0.4.1/docs/FIRST-RUN.md +103 -0
- damien-0.4.1/docs/HOW-IT-WORKS.md +119 -0
- damien-0.4.1/docs/POLICY.md +115 -0
- damien-0.4.1/docs/PRIVACY.md +104 -0
- damien-0.4.1/docs/RUNBOOK.md +157 -0
- damien-0.4.1/docs/SETUP.md +181 -0
- damien-0.4.1/docs/SKILL-PROMPT.md +21 -0
- damien-0.4.1/pyproject.toml +71 -0
- damien-0.4.1/src/damien/__init__.py +42 -0
- damien-0.4.1/src/damien/calibrate.py +138 -0
- damien-0.4.1/src/damien/cli/__init__.py +131 -0
- damien-0.4.1/src/damien/cli/core.py +234 -0
- damien-0.4.1/src/damien/cli/plans.py +271 -0
- damien-0.4.1/src/damien/cli/rules_cmds.py +242 -0
- damien-0.4.1/src/damien/cli/runs.py +118 -0
- damien-0.4.1/src/damien/config.py +219 -0
- damien-0.4.1/src/damien/decide.py +273 -0
- damien-0.4.1/src/damien/doctor.py +139 -0
- damien-0.4.1/src/damien/embed.py +140 -0
- damien-0.4.1/src/damien/gmail.py +439 -0
- damien-0.4.1/src/damien/jev.py +305 -0
- damien-0.4.1/src/damien/mime.py +141 -0
- damien-0.4.1/src/damien/models.py +159 -0
- damien-0.4.1/src/damien/passes.py +173 -0
- damien-0.4.1/src/damien/plan.py +582 -0
- damien-0.4.1/src/damien/profile.py +77 -0
- damien-0.4.1/src/damien/rules.py +413 -0
- damien-0.4.1/src/damien/skill/SKILL.md +55 -0
- damien-0.4.1/src/damien/stakes.py +141 -0
- damien-0.4.1/src/damien/store.py +798 -0
- damien-0.4.1/tests/conftest.py +201 -0
- damien-0.4.1/tests/test_apply_journal.py +179 -0
- damien-0.4.1/tests/test_calibrate.py +66 -0
- damien-0.4.1/tests/test_cli.py +135 -0
- damien-0.4.1/tests/test_doctor.py +86 -0
- damien-0.4.1/tests/test_escalation.py +141 -0
- damien-0.4.1/tests/test_gmail.py +244 -0
- damien-0.4.1/tests/test_judge.py +416 -0
- damien-0.4.1/tests/test_mime.py +87 -0
- damien-0.4.1/tests/test_plan.py +580 -0
- damien-0.4.1/tests/test_profile.py +71 -0
- damien-0.4.1/tests/test_rules.py +307 -0
- damien-0.4.1/tests/test_rules_pass.py +98 -0
- damien-0.4.1/tests/test_setup.py +119 -0
- damien-0.4.1/tests/test_stakes.py +243 -0
- damien-0.4.1/tests/test_store.py +178 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Damien — your install's settings. Never commit real values.
|
|
2
|
+
|
|
3
|
+
# The account this install works on (used to label stored rows).
|
|
4
|
+
DAMIEN_ACCOUNT=
|
|
5
|
+
|
|
6
|
+
# rules = deterministic only, no model calls, no key, nothing sent anywhere
|
|
7
|
+
# openrouter = a decision model through OpenRouter's decisions endpoint (needs OPENROUTER_API_KEY)
|
|
8
|
+
# typesafe = Jev direct from TypeSafe (needs TYPESAFE_API_KEY)
|
|
9
|
+
# local = a decision model you host yourself (needs DAMIEN_JUDGE_URL; DAMIEN_JUDGE_KEY if it asks for one)
|
|
10
|
+
# fake = deterministic stand-in, for a demo with no key
|
|
11
|
+
DAMIEN_JUDGE=rules
|
|
12
|
+
# Any System-One decision model: state + typed questions in, choice/confidence/probabilities out.
|
|
13
|
+
# On openrouter, e.g. typesafe/jev-1.14 or perplexity/pplx-decider-flash-v1.2. A model is only
|
|
14
|
+
# trusted to act unattended once `damien judge eval` has calibrated it (jev-1.13 keeps the
|
|
15
|
+
# 0.9 bar it was tuned at).
|
|
16
|
+
DAMIEN_MODEL=typesafe/jev-1.13
|
|
17
|
+
OPENROUTER_API_KEY=
|
|
18
|
+
# TYPESAFE_API_KEY=
|
|
19
|
+
# DAMIEN_JUDGE_URL=http://127.0.0.1:8080/v1/systemone (overrides the backend's default URL)
|
|
20
|
+
# DAMIEN_JUDGE_KEY=
|
|
21
|
+
# Parallel judgment requests (the public endpoints cap near eight).
|
|
22
|
+
# DAMIEN_JUDGE_CONCURRENCY=8
|
|
23
|
+
|
|
24
|
+
# Unset = one SQLite file at ~/.damien/damien.db. Set = Postgres.
|
|
25
|
+
# DAMIEN_DB_URL=postgres://user:password@127.0.0.1:5432/damien
|
|
26
|
+
|
|
27
|
+
# off | on_escalation. on_escalation reads the newest text of the messages the headers and
|
|
28
|
+
# snippet could not settle, and asks again - the only mode where message text reaches the
|
|
29
|
+
# judge (see docs/PRIVACY.md). What it reads is kept locally; attachments are never opened.
|
|
30
|
+
DAMIEN_READ_BODIES=off
|
|
31
|
+
|
|
32
|
+
# Labels the model may never act against: anything carrying one is kept.
|
|
33
|
+
DAMIEN_PROTECT_LABELS=ACTION REQUIRED,WAITING FOR,Bills
|
|
34
|
+
|
|
35
|
+
# 1 = filing a message (archiving it, labelling it out of the inbox) also marks it read, so
|
|
36
|
+
# "unread" keeps meaning "you have not looked at this yet". Kept mail is never touched.
|
|
37
|
+
# DAMIEN_MARK_READ_WHEN_FILED=0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# A tag `vX.Y.Z` publishes that version to PyPI. Publishing uses PyPI trusted publishing
|
|
4
|
+
# (OIDC): no token is stored in this repository.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
- name: the tag names the version pyproject declares
|
|
16
|
+
run: |
|
|
17
|
+
declared="$(python3 -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
|
|
18
|
+
test "v$declared" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME but pyproject says $declared"; exit 1; }
|
|
19
|
+
- run: uv build
|
|
20
|
+
- name: the wheel carries the agent skill
|
|
21
|
+
run: python3 -m zipfile -l dist/*.whl | grep -q 'damien/skill/SKILL.md'
|
|
22
|
+
- name: the wheel installs and runs
|
|
23
|
+
run: |
|
|
24
|
+
uv venv /tmp/smoke && VIRTUAL_ENV=/tmp/smoke uv pip install dist/*.whl
|
|
25
|
+
test "$(/tmp/smoke/bin/damien version | head -1)" = "damien ${GITHUB_REF_NAME#v}"
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment: pypi
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/download-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest]
|
|
15
|
+
python: ["3.11", "3.13"]
|
|
16
|
+
# Service containers run on Linux runners only: ubuntu covers both store dialects,
|
|
17
|
+
# macOS covers SQLite (the Postgres half skips itself when DAMIEN_TEST_DB_URL is unset).
|
|
18
|
+
services:
|
|
19
|
+
postgres:
|
|
20
|
+
image: ${{ matrix.os == 'ubuntu-latest' && 'postgres:17' || '' }}
|
|
21
|
+
env:
|
|
22
|
+
POSTGRES_USER: damien
|
|
23
|
+
POSTGRES_PASSWORD: damien
|
|
24
|
+
POSTGRES_DB: damien_test
|
|
25
|
+
ports:
|
|
26
|
+
- 5432:5432
|
|
27
|
+
options: >-
|
|
28
|
+
--health-cmd "pg_isready -U damien"
|
|
29
|
+
--health-interval 5s
|
|
30
|
+
--health-timeout 5s
|
|
31
|
+
--health-retries 10
|
|
32
|
+
env:
|
|
33
|
+
DAMIEN_TEST_DB_URL: ${{ matrix.os == 'ubuntu-latest' && 'postgresql://damien:damien@localhost:5432/damien_test' || '' }}
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: ${{ matrix.python }}
|
|
39
|
+
- name: install
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
pip install -e '.[dev]'
|
|
43
|
+
- name: lint
|
|
44
|
+
run: |
|
|
45
|
+
pip install ruff
|
|
46
|
+
ruff check . # the standard lives in pyproject.toml
|
|
47
|
+
- name: test
|
|
48
|
+
run: pytest -q
|
damien-0.4.1/.gitignore
ADDED
damien-0.4.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Rivera
|
|
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.
|
damien-0.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: damien
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: A Gmail triage CLI that asks before it acts: rules first, a model for the residue, and journalled, reversible writes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ivan-rivera-projects/damien
|
|
6
|
+
Project-URL: Repository, https://github.com/ivan-rivera-projects/damien
|
|
7
|
+
Project-URL: Issues, https://github.com/ivan-rivera-projects/damien/issues
|
|
8
|
+
Author: Ivan Rivera
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: automation,cli,email,gmail,inbox,llm,rules,triage
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications :: Email
|
|
22
|
+
Classifier: Topic :: Office/Business
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: google-auth-oauthlib>=1.2
|
|
25
|
+
Requires-Dist: google-auth>=2.30
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: pydantic>=2.7
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Provides-Extra: pg
|
|
33
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'pg'
|
|
34
|
+
Provides-Extra: vectors
|
|
35
|
+
Requires-Dist: fastembed>=0.4; extra == 'vectors'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Damien
|
|
39
|
+
|
|
40
|
+
**A Gmail triage CLI that asks before it acts.** Rules decide what they can, a model judges
|
|
41
|
+
the residue, and code performs every write — in bulk, journalled, and reversible.
|
|
42
|
+
|
|
43
|
+
[](https://github.com/ivan-rivera-projects/damien/actions/workflows/test.yml)
|
|
44
|
+

|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
It was built to answer one question honestly: *can a mailbox of a hundred thousand
|
|
48
|
+
messages be put in order by software you would actually leave alone with your mail?*
|
|
49
|
+
|
|
50
|
+
## Why it is different
|
|
51
|
+
|
|
52
|
+
- **It never acts on mail you have not read.** A plan is a document. Applying one takes an
|
|
53
|
+
approval token — printed once by `plan approve`, stored only as a hash — and
|
|
54
|
+
`--only <action>` applies exactly the slice you looked at.
|
|
55
|
+
- **Every write is reversible.** Each batch is journalled as `pending` *before* it reaches
|
|
56
|
+
Gmail and marked `done` after, so `run undo` puts back the state each message had when the
|
|
57
|
+
plan was built (inbox, unread, label). `trash` is a label and
|
|
58
|
+
Gmail holds it 30 days; there is no permanent-delete path in the code or the scope.
|
|
59
|
+
- **Rules run first, and they are free.** A rule that maps onto Gmail search syntax is
|
|
60
|
+
executed *by Gmail* — no listings, no headers, no model. On a settled mailbox a pass
|
|
61
|
+
costs **$0**.
|
|
62
|
+
- **The model only ever sees what your rules could not settle** — headers and a snippet —
|
|
63
|
+
and every answer is cached, so a message is never paid for twice. It reads the *text* only
|
|
64
|
+
if you ask it to (`DAMIEN_READ_BODIES=on_escalation`), only for the messages the snippet
|
|
65
|
+
could not settle, and never an attachment.
|
|
66
|
+
- **Your keys, your credential, nothing in the middle.** No server, no account here, no
|
|
67
|
+
telemetry. Run it with no model at all if you like (`DAMIEN_JUDGE=rules`).
|
|
68
|
+
- **It does not send.** There is no send *path*: no code in Damien sends, drafts-and-sends,
|
|
69
|
+
or forwards (the `gmail.modify` scope technically permits sending; Damien never calls it).
|
|
70
|
+
|
|
71
|
+
## What it did on a real mailbox
|
|
72
|
+
|
|
73
|
+
One Gmail account, used daily, 107,710 messages in the inbox when this started. Every figure
|
|
74
|
+
below is measured by the tool itself, not estimated:
|
|
75
|
+
|
|
76
|
+
| | |
|
|
77
|
+
|---|---|
|
|
78
|
+
| **107,710 → under 7,000** messages in the inbox | 94,202 of them were 2024 or older |
|
|
79
|
+
| **101,048** messages acted on, across **29** applied runs | each one bulk, journalled, reversible |
|
|
80
|
+
| **44,985** judgements by the model · **27,072** decisions by rules | rules are mined from your own corrections — and they are free |
|
|
81
|
+
| **200 corrections → 68 rules**, 67 of them evaluated by Gmail itself | so 36% of all decisions now cost nothing and never drift |
|
|
82
|
+
| **$0.0000449 per judgement** | ≈**$0.045 per 1,000 messages**; a 7,054-message inbox took **$0.17** and about three minutes |
|
|
83
|
+
| **$0.000000** once the rules cover it | a settled pass re-decided 233 messages in **3 seconds**, 0 model calls |
|
|
84
|
+
| **44,985 judgements ≈ $2.02**, best case | and many were served from cache at no cost at all |
|
|
85
|
+
| a 200-message plan cold: **~10 s, $0.0075** · warm: **~1.0 s, $0.0000** | the cache key is the request, so unchanged mail is never bought twice |
|
|
86
|
+
|
|
87
|
+
That table is the whole argument for the architecture: **the expensive part shrinks as you
|
|
88
|
+
use it**, because every class of mail you decide once becomes a rule that costs nothing
|
|
89
|
+
forever. The model is the on-ramp, not the engine.
|
|
90
|
+
|
|
91
|
+
## What it looks like
|
|
92
|
+
|
|
93
|
+
From a real run over the author's mailbox (7,054 messages still in the inbox; 68 rules;
|
|
94
|
+
about three minutes):
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
$ damien plan build -q 'in:inbox' --show 12
|
|
98
|
+
plan 8f6ed799980e (7054 item(s))
|
|
99
|
+
archive 49
|
|
100
|
+
keep 6825
|
|
101
|
+
label 66
|
|
102
|
+
trash 114
|
|
103
|
+
judgment: 3709 call(s), 575 from cache, $0.1713
|
|
104
|
+
archive jev 121d 'Clean up your report, Ivan.' <notifications@mail.creditkarma.com>
|
|
105
|
+
keep rule 117d 'AI is making your next Apple device cost more' <theneuron@newsletter.theneurondaily.com>
|
|
106
|
+
label Receipts jev 13d 'You accepted an AWS Marketplace offer' <no-reply@marketplace.aws>
|
|
107
|
+
trash jev 13d '💀 You're Not Ready for This Friday…' <support@theduabrand.com>
|
|
108
|
+
|
|
109
|
+
$ damien plan approve 8f6ed799980e
|
|
110
|
+
token: 1f0c…
|
|
111
|
+
|
|
112
|
+
$ damien plan apply 8f6ed799980e --token 1f0c… --yes --only trash
|
|
113
|
+
run 2ba1162fdb34 ok: 114 message(s) in 1 batch(es) (trash slice)
|
|
114
|
+
the rest of the plan is still approved
|
|
115
|
+
to take it back: damien run undo 2ba1162fdb34 --yes
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Nothing in Gmail changed until that last command, and `run undo 2ba1162fdb34` reverses it.
|
|
119
|
+
|
|
120
|
+
## Install
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uvx --from git+https://github.com/ivan-rivera-projects/damien damien --help # try it without installing
|
|
124
|
+
pipx install git+https://github.com/ivan-rivera-projects/damien # or install it
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
From a checkout, if you want to hack on it:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/ivan-rivera-projects/damien && cd damien
|
|
131
|
+
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
State lives in `~/.damien` (one SQLite file by default; set `DAMIEN_DB_URL` for Postgres).
|
|
135
|
+
|
|
136
|
+
## Set it up
|
|
137
|
+
|
|
138
|
+
Two things are yours, and neither is shared with anyone else:
|
|
139
|
+
|
|
140
|
+
1. **A Gmail credential of your own** — a Google OAuth *desktop* client from your own
|
|
141
|
+
Cloud project, dropped at `~/.damien/credentials.json`, then `damien login`. One scope
|
|
142
|
+
(`gmail.modify`), and you can revoke it whenever you like.
|
|
143
|
+
2. **A judge, or none** — any decision model with the Jev contract: `openrouter`
|
|
144
|
+
(`DAMIEN_MODEL` + `OPENROUTER_API_KEY`), `typesafe` (`TYPESAFE_API_KEY`), `local`
|
|
145
|
+
(`DAMIEN_JUDGE_URL`; nothing leaves the machine), or `DAMIEN_JUDGE=rules` to use no model
|
|
146
|
+
at all and pay nothing. `damien judge eval` then calibrates the threshold for your model.
|
|
147
|
+
|
|
148
|
+
Then `damien init` writes your profile and a rules file, and `damien doctor` tells you
|
|
149
|
+
whether anything is missing (it changes nothing).
|
|
150
|
+
|
|
151
|
+
→ **[`docs/SETUP.md`](docs/SETUP.md)** has the click-by-click, including the OAuth
|
|
152
|
+
testing-mode trap that expires tokens weekly (it caught this project's own first
|
|
153
|
+
credential). → **[`docs/FIRST-RUN.md`](docs/FIRST-RUN.md)** is the first hour, in order.
|
|
154
|
+
|
|
155
|
+
## The pipeline
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
scan ──► rules ──► (residue) ──► the judge ──► guardrail ──► confidence ──► plan ──► apply ──► journal
|
|
159
|
+
free one request a person's threshold review bulk undoable
|
|
160
|
+
server- per message signals win groups
|
|
161
|
+
side
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
1. **Rules first, and for free.** A rule that maps onto Gmail search syntax is executed *by
|
|
165
|
+
Gmail*. `to_query()` returns `None` when it cannot be certain, and the rule then runs in
|
|
166
|
+
Python over listed mail — a wrong query acts on the wrong mail, so `None` is the safe
|
|
167
|
+
direction.
|
|
168
|
+
2. **Gmail's own work is trusted before ours.** Spam is never fetched. `CATEGORY_PERSONAL`
|
|
169
|
+
is kept by a rule with no model call. Gmail's category reaches the model as context.
|
|
170
|
+
3. **The person's work is trusted above both.** Labels you name in
|
|
171
|
+
`DAMIEN_PROTECT_LABELS` are a floor the model cannot cross, your labels reach the model
|
|
172
|
+
as `your_labels`, and your own corrections outrank a rule and the model on every later
|
|
173
|
+
plan.
|
|
174
|
+
4. **The judge sees only the residue**, as a small state (headers + snippet, never the
|
|
175
|
+
body, unless escalation is on) and *atomic* questions: which bucket, was it sent by a machine, does it want a
|
|
176
|
+
reply, how important. One request per message. The answers are kept as data, so
|
|
177
|
+
thresholds can be re-tuned without paying again.
|
|
178
|
+
5. **A guardrail may only raise an action to `keep`** — never overrule you or a rule. It
|
|
179
|
+
fires on money and deadline subjects, replies from real people, mail you already read,
|
|
180
|
+
and Gmail's own `IMPORTANT`.
|
|
181
|
+
6. **A confidence gate.** Below `DAMIEN_CONFIDENCE_ASK` a judgement keeps the message with
|
|
182
|
+
the reason attached: recorded, but not acted on.
|
|
183
|
+
7. **Plans are read, then applied** — by group or by action, with every batch journalled
|
|
184
|
+
`pending` before it lands and `done` after, so a run is resumable and undoable.
|
|
185
|
+
|
|
186
|
+
## Use
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
damien doctor # is everything wired up?
|
|
190
|
+
damien scan -q 'in:inbox older_than:2y' # how much is there? reads only
|
|
191
|
+
damien plan build -q 'in:inbox' -n 200 # decide; writes nothing to Gmail
|
|
192
|
+
damien plan digest <plan-id> --min-size 5 # collapse it into review groups (N:fp)
|
|
193
|
+
damien plan approve <plan-id> # prints a token, once; only its hash is kept
|
|
194
|
+
damien plan apply <plan-id> --token <t> --only archive --yes
|
|
195
|
+
damien run undo <run-id> --yes # take a run back
|
|
196
|
+
damien correct <message-id> --action keep --note 'a brand we follow'
|
|
197
|
+
damien rules from-senders <plan-id> --yes # make your decisions permanent
|
|
198
|
+
damien rules plan --scope 'in:inbox older_than:2y' # a rule pass over history, bounded
|
|
199
|
+
damien judge eval # calibrate the model's threshold
|
|
200
|
+
damien pass weekly # the scheduled pass: build, approve, apply the certain
|
|
201
|
+
damien skill install --target openclaw # give an agent host the Damien skill
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`--json` on `status`, `doctor`, `scan`, `plan build|digest|show|approve|apply|apply-group`,
|
|
205
|
+
`run list|undo`, `rules list`, `correct`, `restore`, `judge eval`, `pass`.
|
|
206
|
+
Safe to run twice: `plan build` never writes to Gmail, an applied plan refuses to re-apply,
|
|
207
|
+
a partial run resumes only when its filter matches and only the batches its journal does not
|
|
208
|
+
hold, and an undone run cannot be undone twice.
|
|
209
|
+
|
|
210
|
+
## Documentation
|
|
211
|
+
|
|
212
|
+
| document | read it for |
|
|
213
|
+
|---|---|
|
|
214
|
+
| [`docs/SETUP.md`](docs/SETUP.md) | your own Gmail credential and judge key, step by step |
|
|
215
|
+
| [`docs/FIRST-RUN.md`](docs/FIRST-RUN.md) | the first hour: look, apply, learn rules, schedule |
|
|
216
|
+
| [`docs/COMMANDS.md`](docs/COMMANDS.md) | every command — when to use it, what it writes, what it costs |
|
|
217
|
+
| [`docs/HOW-IT-WORKS.md`](docs/HOW-IT-WORKS.md) | the pipeline, the store, and what it refuses to do |
|
|
218
|
+
| [`docs/POLICY.md`](docs/POLICY.md) | the layers of authority, the seven buckets, the rule vocabulary |
|
|
219
|
+
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md) | the weekly and monthly passes, failure modes, how to undo |
|
|
220
|
+
| [`docs/PRIVACY.md`](docs/PRIVACY.md) | exactly what leaves your machine, and when |
|
|
221
|
+
|
|
222
|
+
Two more are for machines or reviewers rather than users: `docs/SKILL-PROMPT.md` is a prompt
|
|
223
|
+
that has an AI agent (Hermes, OpenClaw, anything that runs commands) create its own skill for
|
|
224
|
+
driving this, including the scheduled passes below, and `docs/AUDIT-BRIEF.md` is a brief for an
|
|
225
|
+
independent reviewer — the invariants this tool promises, where each is enforced, and the
|
|
226
|
+
surfaces where a defect would hide.
|
|
227
|
+
|
|
228
|
+
## Cleaning a mailbox of a hundred thousand messages
|
|
229
|
+
|
|
230
|
+
Brute force does not work, and the numbers say why: a Gmail header costs quota, and a
|
|
231
|
+
first attempt to fetch a whole inbox throttled after 64 seconds. A paced pass over 107,710
|
|
232
|
+
messages would take ~90 minutes of wall time before any judgement happened.
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
107,710 in:inbox
|
|
236
|
+
94,202 2024 and older <- 87% of it, historical records
|
|
237
|
+
2,256 Jan – Oct 2025
|
|
238
|
+
1,717 Oct 21 – Dec 2025
|
|
239
|
+
9,536 2026
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
So the order is: **rule passes first, judgement second, and never both over the same
|
|
243
|
+
messages.**
|
|
244
|
+
|
|
245
|
+
| step | how | messages | cost |
|
|
246
|
+
|---|---|---|---|
|
|
247
|
+
| archive what a rule can describe | `rules plan --scope …` — Gmail evaluates it | tens of thousands | ~1 min, $0, no headers |
|
|
248
|
+
| judge what is left and recent | `plan build -q '<a date range>'` | a few thousand | ~$0.000046 each |
|
|
249
|
+
| keep it clean | `rules from-senders` on the recurring senders | — | free |
|
|
250
|
+
|
|
251
|
+
The rule query is always wrapped in `in:inbox` unless `--scope` says otherwise: a bare
|
|
252
|
+
`older_than:365d` matched 152,881 messages in this mailbox because it had escaped the inbox
|
|
253
|
+
and was reaching Sent and archived mail too.
|
|
254
|
+
|
|
255
|
+
## Where state lives
|
|
256
|
+
|
|
257
|
+
| | SQLite (default) | Postgres (`DAMIEN_DB_URL`) |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| setup | none — one file | one env line |
|
|
260
|
+
| vectors | text column, clustered in Python | `message_embeddings` + `pgvector` |
|
|
261
|
+
| tested | ✅ | ✅ in CI: Linux runners start a `postgres:17` service; macOS runs SQLite only |
|
|
262
|
+
|
|
263
|
+
Tables: `messages`, `decisions`, `verdicts` (the judgement cache), `message_embeddings`,
|
|
264
|
+
`plans`, `plan_items`, `runs`, `run_batches`, `corrections`. A column added after release is
|
|
265
|
+
applied by `ADDED_COLUMNS` in `store.py` — never by dropping the table.
|
|
266
|
+
|
|
267
|
+
## The judgement cache
|
|
268
|
+
|
|
269
|
+
The cache key *is* the request: the model, the questions as written, and the state the model
|
|
270
|
+
was shown. Reword a question, change the model, or add a label yourself — invalidated.
|
|
271
|
+
Nothing else. That is why a settled mailbox costs nothing to re-check, and why editing the
|
|
272
|
+
buckets in `decide.py` is the one change that re-buys every answer.
|
|
273
|
+
|
|
274
|
+
## Review groups (the digest)
|
|
275
|
+
|
|
276
|
+
Senders do not collapse an inbox: 200 messages came from 140 senders, 102 with a single
|
|
277
|
+
message. Similarity does. Local embeddings (`fastembed`, no torch), greedy
|
|
278
|
+
nearest-centroid, and a threshold chosen from the data — 0.65 on this mailbox, where 0.70
|
|
279
|
+
left 75 groups and 0.60 dissolved the useful "4 finance offers" group into the marketing
|
|
280
|
+
blob. Result: 200 messages → 36 review groups.
|
|
281
|
+
|
|
282
|
+
## What this deliberately does not do
|
|
283
|
+
|
|
284
|
+
- **No bodies unless you ask.** Judgement uses headers and a snippet. With
|
|
285
|
+
`DAMIEN_READ_BODIES=on_escalation` it also reads the *newest text* of the messages the
|
|
286
|
+
snippet could not settle — quoted replies stripped, capped, attachments never opened — and
|
|
287
|
+
keeps what it read on your machine so a re-plan costs nothing. Off by default.
|
|
288
|
+
- **No scheduler.** Have the host's own scheduler run `damien pass weekly` every Monday and
|
|
289
|
+
`damien pass monthly` on the 1st (see the RUNBOOK); the tool exits either way.
|
|
290
|
+
- **No permanent delete.** `trash` is recoverable for 30 days, and the code cannot do more.
|
|
291
|
+
- **No sending.** No send path: nothing sends, drafts-and-sends or forwards (the scope
|
|
292
|
+
technically permits sending; Damien never calls it).
|
|
293
|
+
- **No telemetry.** Nothing is reported anywhere.
|
|
294
|
+
- **No MCP door.** A small tool wrapper over these functions is the obvious next step for a
|
|
295
|
+
host that cannot shell out; the CLI costs zero tool schemas in a context.
|
|
296
|
+
|
|
297
|
+
## Model facts this is built on (all from the vendor docs)
|
|
298
|
+
|
|
299
|
+
- One request per message with *every* question about it: questions in a call are
|
|
300
|
+
independent and parallel ("adding questions barely changes the response time"), while
|
|
301
|
+
many emails in one `state` costs accuracy ("Jev suffers from context rot").
|
|
302
|
+
- `confidence` (0–1) describes how peaked the answer is, **not** whether it is right —
|
|
303
|
+
hence conservative bands and a person as the last resort.
|
|
304
|
+
- $0.042/Mtok input, output free; the vendor's published measurement is ≈$0.0256 per 1,000
|
|
305
|
+
items, and this tool measured ≈$0.000046 per message on a real mailbox.
|
|
306
|
+
- A Choice always picks an option even when none fit, so a companion Noul and an `unclear`
|
|
307
|
+
bucket guard it.
|
|
308
|
+
- Never ask it to count, order or compute — that is code's job.
|
|
309
|
+
|
|
310
|
+
### How the model actually behaved, measured
|
|
311
|
+
|
|
312
|
+
Across **44,985 judgements** on one real mailbox:
|
|
313
|
+
|
|
314
|
+
| | | |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| **10,735** | 24% | cleared the 0.8 bar and were **acted on unattended** |
|
|
317
|
+
| **11,611** | 26% | between the bars — reported, not acted on |
|
|
318
|
+
| **22,639** | 50% | **kept**, because it was not sure enough to touch anything |
|
|
319
|
+
| **36,775** (82%) | | of what it chose was `keep`; `trash` was only 5.5% of its answers |
|
|
320
|
+
|
|
321
|
+
Two things follow, and they are the reason this design is safe to leave running:
|
|
322
|
+
|
|
323
|
+
- **Half the model's answers never moved a message.** The confidence gate is not decoration:
|
|
324
|
+
a judgement below `DAMIEN_CONFIDENCE_ASK` is recorded and the message stays where it is.
|
|
325
|
+
- **The model is conservative, and the rules do the deleting.** Its instinct is `keep`
|
|
326
|
+
(82%); the 27,072 decisions made by rules — mined from your own corrections, and evaluated
|
|
327
|
+
by Gmail — are where the archive-and-trash volume actually lives. You decide the classes;
|
|
328
|
+
the model only handles what is left, and only when it is sure.
|
|
329
|
+
|
|
330
|
+
**200 corrections** were enough to produce **68 rules**, which is **0.44% of judgements**
|
|
331
|
+
touched by hand. That is the whole loop: correct a class once, and it never costs a model
|
|
332
|
+
call again.
|
|
333
|
+
|
|
334
|
+
## Status
|
|
335
|
+
|
|
336
|
+
Beta, and in daily use on a 100,000-message mailbox. The test suite runs on Linux and macOS;
|
|
337
|
+
Postgres is exercised on the Linux runners. Interfaces may still move; the safety properties above will
|
|
338
|
+
not.
|
|
339
|
+
|
|
340
|
+
MIT licensed. The predecessor project — a four-service platform that this replaced — was
|
|
341
|
+
torn down in October 2026; its repositories remain on GitHub for history and nothing of it
|
|
342
|
+
runs.
|