acquaint 0.0.2__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.
- acquaint-0.0.2/.github/workflows/ci.yml +43 -0
- acquaint-0.0.2/.gitignore +120 -0
- acquaint-0.0.2/LICENSE +21 -0
- acquaint-0.0.2/PKG-INFO +168 -0
- acquaint-0.0.2/README.md +141 -0
- acquaint-0.0.2/acquaint/__init__.py +63 -0
- acquaint-0.0.2/acquaint/__main__.py +73 -0
- acquaint-0.0.2/acquaint/brief.py +243 -0
- acquaint-0.0.2/acquaint/data/agents/profile-reader.md +18 -0
- acquaint-0.0.2/acquaint/data/agents/recipient-reader.md +16 -0
- acquaint-0.0.2/acquaint/data/deslop/tells.yaml +164 -0
- acquaint-0.0.2/acquaint/data/hooks/pre-push +48 -0
- acquaint-0.0.2/acquaint/data/policy.yaml +55 -0
- acquaint-0.0.2/acquaint/data/purposes.yaml +70 -0
- acquaint-0.0.2/acquaint/data/skills/acquaint/SKILL.md +66 -0
- acquaint-0.0.2/acquaint/data/skills/acquaint-profile/SKILL.md +126 -0
- acquaint-0.0.2/acquaint/data/skills/acquaint-read/SKILL.md +53 -0
- acquaint-0.0.2/acquaint/data/skills/acquaint-sync/SKILL.md +62 -0
- acquaint-0.0.2/acquaint/data/skills/acquaint-write/SKILL.md +79 -0
- acquaint-0.0.2/acquaint/data/skills/deslop/SKILL.md +74 -0
- acquaint-0.0.2/acquaint/data/templates/POLICY.md +44 -0
- acquaint-0.0.2/acquaint/data/templates/ledger.md +20 -0
- acquaint-0.0.2/acquaint/data/templates/person.md +29 -0
- acquaint-0.0.2/acquaint/deslop.py +262 -0
- acquaint-0.0.2/acquaint/edit.py +570 -0
- acquaint-0.0.2/acquaint/lint.py +384 -0
- acquaint-0.0.2/acquaint/lookup.py +483 -0
- acquaint-0.0.2/acquaint/mcp.py +102 -0
- acquaint-0.0.2/acquaint/records.py +557 -0
- acquaint-0.0.2/acquaint/render.py +53 -0
- acquaint-0.0.2/acquaint/resources.py +39 -0
- acquaint-0.0.2/acquaint/store.py +611 -0
- acquaint-0.0.2/acquaint/sync.py +493 -0
- acquaint-0.0.2/acquaint/tools.py +598 -0
- acquaint-0.0.2/pyproject.toml +185 -0
- acquaint-0.0.2/tests/conftest.py +26 -0
- acquaint-0.0.2/tests/test_deslop.py +111 -0
- acquaint-0.0.2/tests/test_edit_lint_brief.py +380 -0
- acquaint-0.0.2/tests/test_lookup.py +181 -0
- acquaint-0.0.2/tests/test_no_personal_data.py +114 -0
- acquaint-0.0.2/tests/test_records_store.py +273 -0
- acquaint-0.0.2/tests/test_skills.py +83 -0
- acquaint-0.0.2/tests/test_smoke.py +87 -0
- acquaint-0.0.2/tests/test_surfaces.py +118 -0
- acquaint-0.0.2/tests/test_sync.py +325 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# wads CI — calls the reusable workflow hosted in i2mint/wads.
|
|
2
|
+
#
|
|
3
|
+
# All configuration comes from this repo's pyproject.toml [tool.wads.ci.*].
|
|
4
|
+
# To customize the workflow itself (rare), replace this file with the
|
|
5
|
+
# full inline template `wads/data/github_ci_uv.yml` from i2mint/wads.
|
|
6
|
+
#
|
|
7
|
+
# Pinning: `@master` floats with wads. If you need version stability for
|
|
8
|
+
# a release-sensitive repo, change `@master` to a wads tag (e.g. `@0.2.15`;
|
|
9
|
+
# tags have no `v` prefix). A stub whose `secrets:` block passes the JSON
|
|
10
|
+
# transport (the default below) needs a tag from a release after 0.2.14 —
|
|
11
|
+
# older tags don't declare that secret and GitHub then rejects the
|
|
12
|
+
# workflow at parse time.
|
|
13
|
+
# CI failure does not block a published release — it blocks the publish
|
|
14
|
+
# step itself — so floating master is generally safe.
|
|
15
|
+
#
|
|
16
|
+
# Permissions: GitHub validates that the caller grants AT LEAST the
|
|
17
|
+
# permissions any job in the called workflow requests — at workflow-parse
|
|
18
|
+
# time, not at run-time, even if the job would be skipped via `if:`.
|
|
19
|
+
# The reusable workflow needs:
|
|
20
|
+
# contents: write for the publish job's version-bump push-back
|
|
21
|
+
# and for the github-pages job's gh-pages branch push
|
|
22
|
+
# pages: write for the github-pages job's REST API Pages config
|
|
23
|
+
# Both default to `write` on org-account GITHUB_TOKEN and need to be
|
|
24
|
+
# granted explicitly on personal-account callers (where the default is
|
|
25
|
+
# read-only). No `id-token: write` needed — the publish-github-pages
|
|
26
|
+
# action uses peaceiris/actions-gh-pages (branch-based) + REST API,
|
|
27
|
+
# not the OIDC `actions/deploy-pages` flow.
|
|
28
|
+
name: Continuous Integration
|
|
29
|
+
on: [push, pull_request]
|
|
30
|
+
jobs:
|
|
31
|
+
ci:
|
|
32
|
+
uses: i2mint/wads/.github/workflows/uv-ci.yml@master
|
|
33
|
+
permissions:
|
|
34
|
+
contents: write
|
|
35
|
+
pages: write
|
|
36
|
+
# Transport (NAMED, legacy): explicitly passes only the secrets
|
|
37
|
+
# listed below (PYPI_PASSWORD + those declared in
|
|
38
|
+
# [tool.wads.ci.env]). Every name must be in the frozen wads
|
|
39
|
+
# superset (wads/ci_secrets.py) or GitHub rejects the workflow
|
|
40
|
+
# at parse time. The default JSON transport has no such limit;
|
|
41
|
+
# regenerate with `wads-migrate ci-to-stub` to switch.
|
|
42
|
+
secrets:
|
|
43
|
+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
.claude/handoffs/
|
|
2
|
+
.claude/scratch/
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
.DS_Store
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
|
|
14
|
+
# TLS certificates
|
|
15
|
+
## Ignore all PEM files anywhere
|
|
16
|
+
*.pem
|
|
17
|
+
## Also ignore any certs directory
|
|
18
|
+
certs/
|
|
19
|
+
|
|
20
|
+
# Distribution / packaging
|
|
21
|
+
.Python
|
|
22
|
+
build/
|
|
23
|
+
develop-eggs/
|
|
24
|
+
dist/
|
|
25
|
+
downloads/
|
|
26
|
+
eggs/
|
|
27
|
+
.eggs/
|
|
28
|
+
lib/
|
|
29
|
+
lib64/
|
|
30
|
+
parts/
|
|
31
|
+
sdist/
|
|
32
|
+
var/
|
|
33
|
+
wheels/
|
|
34
|
+
*.egg-info/
|
|
35
|
+
.installed.cfg
|
|
36
|
+
*.egg
|
|
37
|
+
MANIFEST
|
|
38
|
+
_build
|
|
39
|
+
|
|
40
|
+
# PyInstaller
|
|
41
|
+
# Usually these files are written by a python script from a template
|
|
42
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
43
|
+
*.manifest
|
|
44
|
+
*.spec
|
|
45
|
+
|
|
46
|
+
# Installer logs
|
|
47
|
+
pip-log.txt
|
|
48
|
+
pip-delete-this-directory.txt
|
|
49
|
+
|
|
50
|
+
# Unit test / coverage reports
|
|
51
|
+
htmlcov/
|
|
52
|
+
.tox/
|
|
53
|
+
.coverage
|
|
54
|
+
.coverage.*
|
|
55
|
+
.cache
|
|
56
|
+
nosetests.xml
|
|
57
|
+
coverage.xml
|
|
58
|
+
*.cover
|
|
59
|
+
.hypothesis/
|
|
60
|
+
.pytest_cache/
|
|
61
|
+
|
|
62
|
+
# Translations
|
|
63
|
+
*.mo
|
|
64
|
+
*.pot
|
|
65
|
+
|
|
66
|
+
# Django stuff:
|
|
67
|
+
*.log
|
|
68
|
+
local_settings.py
|
|
69
|
+
db.sqlite3
|
|
70
|
+
|
|
71
|
+
# Flask stuff:
|
|
72
|
+
instance/
|
|
73
|
+
.webassets-cache
|
|
74
|
+
|
|
75
|
+
# Scrapy stuff:
|
|
76
|
+
.scrapy
|
|
77
|
+
|
|
78
|
+
# Sphinx documentation
|
|
79
|
+
docs/_build/
|
|
80
|
+
docs/*
|
|
81
|
+
|
|
82
|
+
# PyBuilder
|
|
83
|
+
target/
|
|
84
|
+
|
|
85
|
+
# Jupyter Notebook
|
|
86
|
+
.ipynb_checkpoints
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
.python-version
|
|
90
|
+
|
|
91
|
+
# celery beat schedule file
|
|
92
|
+
celerybeat-schedule
|
|
93
|
+
|
|
94
|
+
# SageMath parsed files
|
|
95
|
+
*.sage.py
|
|
96
|
+
|
|
97
|
+
# Environments
|
|
98
|
+
.env
|
|
99
|
+
.venv
|
|
100
|
+
env/
|
|
101
|
+
venv/
|
|
102
|
+
ENV/
|
|
103
|
+
env.bak/
|
|
104
|
+
venv.bak/
|
|
105
|
+
|
|
106
|
+
# Spyder project settings
|
|
107
|
+
.spyderproject
|
|
108
|
+
.spyproject
|
|
109
|
+
|
|
110
|
+
# Rope project settings
|
|
111
|
+
.ropeproject
|
|
112
|
+
|
|
113
|
+
# mkdocs documentation
|
|
114
|
+
/site
|
|
115
|
+
|
|
116
|
+
# mypy
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
|
|
119
|
+
# PyCharm
|
|
120
|
+
.idea
|
acquaint-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thor Whalen
|
|
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.
|
acquaint-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: acquaint
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: People, and what they are involved in, for AI agents: who someone is, how to reach them, how to read them, how to write to them.
|
|
5
|
+
Project-URL: Homepage, https://github.com/thorwhalen/acquaint
|
|
6
|
+
Project-URL: Repository, https://github.com/thorwhalen/acquaint
|
|
7
|
+
Project-URL: Documentation, https://thorwhalen.github.io/acquaint
|
|
8
|
+
Project-URL: Issues, https://github.com/thorwhalen/acquaint/issues
|
|
9
|
+
Author: Thor Whalen
|
|
10
|
+
License: mit
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,claude-code,contacts,deslop,people,profiles,writing
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: cw<0.2,>=0.1.1
|
|
15
|
+
Requires-Dist: dol
|
|
16
|
+
Requires-Dist: pyyaml
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
21
|
+
Provides-Extra: docs
|
|
22
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
|
|
23
|
+
Requires-Dist: sphinx>=6.0; extra == 'docs'
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: py2mcp; extra == 'mcp'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# acquaint
|
|
29
|
+
|
|
30
|
+
People, and what they are involved in, for AI agents: who someone is, how to reach them, how to read them, how to write to them.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install acquaint
|
|
34
|
+
|
|
35
|
+
acquaint new person "Ada Lovelace"
|
|
36
|
+
acquaint remember ada-lovelace "prefers email for anything with attachments" --source "https://example.org/thread/1"
|
|
37
|
+
acquaint who ada -f aka # → Ada, Lovelace
|
|
38
|
+
acquaint brief ada-lovelace --purpose ask # everything to know before writing to her
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Tell an agent "write to Ada about the export" and it can find out, without being told again, who that is, where to reach her for this, what register to use, what to avoid, and where to record what it learns. The "how" is written down once, per person, and every agent, skill and tool reads it through the same verbs.
|
|
42
|
+
|
|
43
|
+
Records are hand-editable Markdown, one folder per person (or project, org, group), kept **outside any code repository**. Every preference carries its source, and `acquaint lint` fails when one does not.
|
|
44
|
+
|
|
45
|
+
## What it records, and what it never does
|
|
46
|
+
|
|
47
|
+
A profile holds observable behaviour with evidence: "replies in one or two lines", "wants the decision first", quoted and linked. It never holds personality labels, moods, or special-category data (health, religion, politics, ethnicity, sexuality, union membership), stated or inferred, nor credentials, identifiers, or whole message bodies. Every store gets a `POLICY.md` stating this, and `lint` warns on tripwires.
|
|
48
|
+
|
|
49
|
+
The test for every line: would it survive being handed to the person it is about?
|
|
50
|
+
|
|
51
|
+
## Where the data lives
|
|
52
|
+
|
|
53
|
+
The data root is the first of: the `data_dir` argument, `$ACQUAINT_DATA_DIR`, `data_dir` in `~/.config/acquaint/config.toml`, `~/.local/share/acquaint`.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
POLICY.md what may be recorded
|
|
57
|
+
people/ada-lovelace/
|
|
58
|
+
PROFILE.md entry file: identity frontmatter + Who · Reach · Write to them · Read them · Don't · Now · More
|
|
59
|
+
identities.yaml handles and addresses, with evidence
|
|
60
|
+
rules.yaml channel rules: when → do, who set it, source
|
|
61
|
+
links.yaml affiliations: project, org, group; role; period
|
|
62
|
+
style.md the writing card: AI tolerance, register, do, don't, blocklist, exemplars
|
|
63
|
+
views.md positions and standing objections, sourced
|
|
64
|
+
sources.md where their writing lives, how authorship was verified
|
|
65
|
+
log/2026-09.md append-only observations
|
|
66
|
+
projects/<id>/PROFILE.md What & status · Where things live · Who · Agents & skills · Norms · Now
|
|
67
|
+
orgs/<id>/ groups/<id>/ … any other kind
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Only `PROFILE.md` is required. A malformed file is reported and skipped; it never breaks lookups of anyone else.
|
|
71
|
+
|
|
72
|
+
## The verbs
|
|
73
|
+
|
|
74
|
+
The same fifteen functions are the Python API (`acquaint.tools`), the CLI, and the MCP tools. Each returns a JSON-ready dict.
|
|
75
|
+
|
|
76
|
+
| Verb | Does |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `who NAME [-f FIELD] [-b]` | one field, the identity block, or the whole entry file; lists candidates instead of guessing |
|
|
79
|
+
| `resolve HANDLE` | `github:octocat`, `email:…` → the person, with the evidence; a handle without its platform, a match by name only, or an inactive identity is reported, never acted on |
|
|
80
|
+
| `check TEXT` | before publishing: one person written as two ("Ada or Lovelace"), shared names, unknown names |
|
|
81
|
+
| `reach PERSON [--purpose --urgency --project --message-type --topic]` | ordered channels: the person's own rules > the operator's rules > project norms > observed habits > defaults |
|
|
82
|
+
| `brief PERSON [--purpose --project]` | card, writing style, views, reach, project norms, recent observations, reminders, and what is **not** known |
|
|
83
|
+
| `remember ENTITY TEXT [--source --kind]` | append a dated, sourced observation (or identity, preference, view, rule) |
|
|
84
|
+
| `lint [ENTITY]` | sources on every preference; parseable files; entry-file budget; policy tripwires |
|
|
85
|
+
| `style-lint TEXT [--recipient --tolerance]` | machine-writing tells, enforced by the reader's tolerance of AI-sounding text |
|
|
86
|
+
| `new KIND NAME [--qualifier --description]` | scaffold from a template; readable slug ids (`ada-lovelace`, `john-smith--example-org`) |
|
|
87
|
+
| `rename ID TO` | new id or name; links elsewhere rewritten (never inside URLs or logs); old forms kept as aliases |
|
|
88
|
+
| `forget ID [--confirm]` | remove the whole folder, leaving a salted tombstone so the person is not silently re-created |
|
|
89
|
+
| `sync init --repo OWNER/NAME` · `sync push` · `sync pull` · `sync status` | private-repository sync, below |
|
|
90
|
+
|
|
91
|
+
`--json` prints the result dict; `-` as the text of `check` or `style-lint` reads stdin.
|
|
92
|
+
|
|
93
|
+
Nothing acts on a guess: a name, alias, handle or email counts only when exactly one record has it exactly, and a partial match comes back as a suggestion. `rename` and `forget` need the exact id.
|
|
94
|
+
|
|
95
|
+
## Sources
|
|
96
|
+
|
|
97
|
+
End every preference, view or rule with a source tag:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
## Write to them
|
|
101
|
+
- Lead with the decision, then the options. [source: self: "give me the recommendation first"]
|
|
102
|
+
- Short replies on chat, fuller ones by email. [source: log/2026-09.md#e03]
|
|
103
|
+
- No attachments over chat. [source: https://example.org/thread/1]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A source is a permalink, a log or research anchor, the person's own words, `operator`, or `none located`. **A date alone is not a source**, and neither is a placeholder such as `unknown` or `TODO`: an unsourced claim with a date attached reads as observed when it was not. A nested bullet is its own line and needs its own source.
|
|
107
|
+
|
|
108
|
+
## Agent skills
|
|
109
|
+
|
|
110
|
+
Six skills ship inside the package (`acquaint/data/skills/`) and install with `gh skill`:
|
|
111
|
+
|
|
112
|
+
| Skill | For |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `acquaint` | the router: lookups at the right cost, `check` before publishing, `remember` with sources |
|
|
115
|
+
| `acquaint-profile` | building a profile from someone's own writing, with parallel `profile-reader` agents |
|
|
116
|
+
| `acquaint-write` | writing for a known reader, and sparring with a simulated one (`recipient-reader` agent) |
|
|
117
|
+
| `acquaint-read` | interpreting a message from a known person; AI-processing as a likelihood with evidence |
|
|
118
|
+
| `deslop` | prose without machine-writing tells, calibrated to the reader |
|
|
119
|
+
| `acquaint-sync` | private sync, and what it does not protect |
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
gh skill install thorwhalen/acquaint acquaint --agent claude-code
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`gh skill` needs a recent `gh`; otherwise symlink the folders from the installed package's `acquaint/data/skills/` into `~/.claude/skills/`. If you already have a user-level skill named `deslop`, skip acquaint's or install it under another name: two skills with the same name at the same scope replace each other.
|
|
126
|
+
|
|
127
|
+
## MCP
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install "acquaint[mcp]"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{"mcpServers": {"acquaint": {"command": "acquaint-mcp"}}}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The server exposes the tools that read locally, append or create (`who`, `resolve`, `check`, `reach`, `brief`, `remember`, `lint`, `new`, `style_lint`). Renaming, forgetting and syncing stay at the terminal. `data_dir` is not exposed: the data root is the server's (set `ACQUAINT_DATA_DIR` in the client configuration), never the model's.
|
|
138
|
+
|
|
139
|
+
## Private sync
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
acquaint sync init --repo <owner>/<name> --dry-run
|
|
143
|
+
acquaint sync init --repo <owner>/<name>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`sync init` creates the repository through `gh` as private, refuses to continue unless `gh` reports it `PRIVATE`, and installs a pre-push hook, active in every worktree, that allows a push only through `origin`, only when `origin` has exactly the one URL recorded at init (no `pushurl` or `pushInsteadOf` redirect), only when that URL names the checked repository on github.com, and only while `gh` still reports it private. `push`, `pull` and `status` check again. `sync init` takes over only an empty folder or a clone of that same repository.
|
|
147
|
+
|
|
148
|
+
What this does **not** protect: a private repository is access control, not encryption (the host can read everything); `git push --no-verify` skips the hook; whoever controls the repository's git config or the `gh` on `PATH` controls what the guard sees; moving the data root disables the hook until `sync init --existing-only` runs again; file names and commit messages contain people's names; deleting a folder does not remove it from history or other clones. `git-remote-gcrypt` would encrypt contents, names and history; it is the planned upgrade, but the guard does not support it yet, so `sync init` refuses `gcrypt::` URLs ([#13](https://github.com/thorwhalen/acquaint/issues/13)).
|
|
149
|
+
|
|
150
|
+
## Python
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from acquaint import Store, who, brief
|
|
154
|
+
|
|
155
|
+
who("ada", field="aka")["value"] # ['Ada', 'Lovelace']
|
|
156
|
+
print(brief("ada-lovelace", purpose="ask")["text"])
|
|
157
|
+
|
|
158
|
+
store = Store() # MutableMapping[str, Entity] over a dol files store
|
|
159
|
+
entity = store[
|
|
160
|
+
"people/ada-lovelace"
|
|
161
|
+
] # a mapping of that person's files, plus parsed views
|
|
162
|
+
entity.identities, entity.rules, entity.sections["Write to them"]
|
|
163
|
+
Store(files={}) # any MutableMapping[str, str] of files: a dict, a remote store
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Design
|
|
167
|
+
|
|
168
|
+
The seams, surfaces and deliberate non-seams are in [Discussion #1](https://github.com/thorwhalen/acquaint/discussions/1).
|
acquaint-0.0.2/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# acquaint
|
|
2
|
+
|
|
3
|
+
People, and what they are involved in, for AI agents: who someone is, how to reach them, how to read them, how to write to them.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install acquaint
|
|
7
|
+
|
|
8
|
+
acquaint new person "Ada Lovelace"
|
|
9
|
+
acquaint remember ada-lovelace "prefers email for anything with attachments" --source "https://example.org/thread/1"
|
|
10
|
+
acquaint who ada -f aka # → Ada, Lovelace
|
|
11
|
+
acquaint brief ada-lovelace --purpose ask # everything to know before writing to her
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Tell an agent "write to Ada about the export" and it can find out, without being told again, who that is, where to reach her for this, what register to use, what to avoid, and where to record what it learns. The "how" is written down once, per person, and every agent, skill and tool reads it through the same verbs.
|
|
15
|
+
|
|
16
|
+
Records are hand-editable Markdown, one folder per person (or project, org, group), kept **outside any code repository**. Every preference carries its source, and `acquaint lint` fails when one does not.
|
|
17
|
+
|
|
18
|
+
## What it records, and what it never does
|
|
19
|
+
|
|
20
|
+
A profile holds observable behaviour with evidence: "replies in one or two lines", "wants the decision first", quoted and linked. It never holds personality labels, moods, or special-category data (health, religion, politics, ethnicity, sexuality, union membership), stated or inferred, nor credentials, identifiers, or whole message bodies. Every store gets a `POLICY.md` stating this, and `lint` warns on tripwires.
|
|
21
|
+
|
|
22
|
+
The test for every line: would it survive being handed to the person it is about?
|
|
23
|
+
|
|
24
|
+
## Where the data lives
|
|
25
|
+
|
|
26
|
+
The data root is the first of: the `data_dir` argument, `$ACQUAINT_DATA_DIR`, `data_dir` in `~/.config/acquaint/config.toml`, `~/.local/share/acquaint`.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
POLICY.md what may be recorded
|
|
30
|
+
people/ada-lovelace/
|
|
31
|
+
PROFILE.md entry file: identity frontmatter + Who · Reach · Write to them · Read them · Don't · Now · More
|
|
32
|
+
identities.yaml handles and addresses, with evidence
|
|
33
|
+
rules.yaml channel rules: when → do, who set it, source
|
|
34
|
+
links.yaml affiliations: project, org, group; role; period
|
|
35
|
+
style.md the writing card: AI tolerance, register, do, don't, blocklist, exemplars
|
|
36
|
+
views.md positions and standing objections, sourced
|
|
37
|
+
sources.md where their writing lives, how authorship was verified
|
|
38
|
+
log/2026-09.md append-only observations
|
|
39
|
+
projects/<id>/PROFILE.md What & status · Where things live · Who · Agents & skills · Norms · Now
|
|
40
|
+
orgs/<id>/ groups/<id>/ … any other kind
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Only `PROFILE.md` is required. A malformed file is reported and skipped; it never breaks lookups of anyone else.
|
|
44
|
+
|
|
45
|
+
## The verbs
|
|
46
|
+
|
|
47
|
+
The same fifteen functions are the Python API (`acquaint.tools`), the CLI, and the MCP tools. Each returns a JSON-ready dict.
|
|
48
|
+
|
|
49
|
+
| Verb | Does |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `who NAME [-f FIELD] [-b]` | one field, the identity block, or the whole entry file; lists candidates instead of guessing |
|
|
52
|
+
| `resolve HANDLE` | `github:octocat`, `email:…` → the person, with the evidence; a handle without its platform, a match by name only, or an inactive identity is reported, never acted on |
|
|
53
|
+
| `check TEXT` | before publishing: one person written as two ("Ada or Lovelace"), shared names, unknown names |
|
|
54
|
+
| `reach PERSON [--purpose --urgency --project --message-type --topic]` | ordered channels: the person's own rules > the operator's rules > project norms > observed habits > defaults |
|
|
55
|
+
| `brief PERSON [--purpose --project]` | card, writing style, views, reach, project norms, recent observations, reminders, and what is **not** known |
|
|
56
|
+
| `remember ENTITY TEXT [--source --kind]` | append a dated, sourced observation (or identity, preference, view, rule) |
|
|
57
|
+
| `lint [ENTITY]` | sources on every preference; parseable files; entry-file budget; policy tripwires |
|
|
58
|
+
| `style-lint TEXT [--recipient --tolerance]` | machine-writing tells, enforced by the reader's tolerance of AI-sounding text |
|
|
59
|
+
| `new KIND NAME [--qualifier --description]` | scaffold from a template; readable slug ids (`ada-lovelace`, `john-smith--example-org`) |
|
|
60
|
+
| `rename ID TO` | new id or name; links elsewhere rewritten (never inside URLs or logs); old forms kept as aliases |
|
|
61
|
+
| `forget ID [--confirm]` | remove the whole folder, leaving a salted tombstone so the person is not silently re-created |
|
|
62
|
+
| `sync init --repo OWNER/NAME` · `sync push` · `sync pull` · `sync status` | private-repository sync, below |
|
|
63
|
+
|
|
64
|
+
`--json` prints the result dict; `-` as the text of `check` or `style-lint` reads stdin.
|
|
65
|
+
|
|
66
|
+
Nothing acts on a guess: a name, alias, handle or email counts only when exactly one record has it exactly, and a partial match comes back as a suggestion. `rename` and `forget` need the exact id.
|
|
67
|
+
|
|
68
|
+
## Sources
|
|
69
|
+
|
|
70
|
+
End every preference, view or rule with a source tag:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
## Write to them
|
|
74
|
+
- Lead with the decision, then the options. [source: self: "give me the recommendation first"]
|
|
75
|
+
- Short replies on chat, fuller ones by email. [source: log/2026-09.md#e03]
|
|
76
|
+
- No attachments over chat. [source: https://example.org/thread/1]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A source is a permalink, a log or research anchor, the person's own words, `operator`, or `none located`. **A date alone is not a source**, and neither is a placeholder such as `unknown` or `TODO`: an unsourced claim with a date attached reads as observed when it was not. A nested bullet is its own line and needs its own source.
|
|
80
|
+
|
|
81
|
+
## Agent skills
|
|
82
|
+
|
|
83
|
+
Six skills ship inside the package (`acquaint/data/skills/`) and install with `gh skill`:
|
|
84
|
+
|
|
85
|
+
| Skill | For |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `acquaint` | the router: lookups at the right cost, `check` before publishing, `remember` with sources |
|
|
88
|
+
| `acquaint-profile` | building a profile from someone's own writing, with parallel `profile-reader` agents |
|
|
89
|
+
| `acquaint-write` | writing for a known reader, and sparring with a simulated one (`recipient-reader` agent) |
|
|
90
|
+
| `acquaint-read` | interpreting a message from a known person; AI-processing as a likelihood with evidence |
|
|
91
|
+
| `deslop` | prose without machine-writing tells, calibrated to the reader |
|
|
92
|
+
| `acquaint-sync` | private sync, and what it does not protect |
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
gh skill install thorwhalen/acquaint acquaint --agent claude-code
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`gh skill` needs a recent `gh`; otherwise symlink the folders from the installed package's `acquaint/data/skills/` into `~/.claude/skills/`. If you already have a user-level skill named `deslop`, skip acquaint's or install it under another name: two skills with the same name at the same scope replace each other.
|
|
99
|
+
|
|
100
|
+
## MCP
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install "acquaint[mcp]"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{"mcpServers": {"acquaint": {"command": "acquaint-mcp"}}}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The server exposes the tools that read locally, append or create (`who`, `resolve`, `check`, `reach`, `brief`, `remember`, `lint`, `new`, `style_lint`). Renaming, forgetting and syncing stay at the terminal. `data_dir` is not exposed: the data root is the server's (set `ACQUAINT_DATA_DIR` in the client configuration), never the model's.
|
|
111
|
+
|
|
112
|
+
## Private sync
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
acquaint sync init --repo <owner>/<name> --dry-run
|
|
116
|
+
acquaint sync init --repo <owner>/<name>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`sync init` creates the repository through `gh` as private, refuses to continue unless `gh` reports it `PRIVATE`, and installs a pre-push hook, active in every worktree, that allows a push only through `origin`, only when `origin` has exactly the one URL recorded at init (no `pushurl` or `pushInsteadOf` redirect), only when that URL names the checked repository on github.com, and only while `gh` still reports it private. `push`, `pull` and `status` check again. `sync init` takes over only an empty folder or a clone of that same repository.
|
|
120
|
+
|
|
121
|
+
What this does **not** protect: a private repository is access control, not encryption (the host can read everything); `git push --no-verify` skips the hook; whoever controls the repository's git config or the `gh` on `PATH` controls what the guard sees; moving the data root disables the hook until `sync init --existing-only` runs again; file names and commit messages contain people's names; deleting a folder does not remove it from history or other clones. `git-remote-gcrypt` would encrypt contents, names and history; it is the planned upgrade, but the guard does not support it yet, so `sync init` refuses `gcrypt::` URLs ([#13](https://github.com/thorwhalen/acquaint/issues/13)).
|
|
122
|
+
|
|
123
|
+
## Python
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from acquaint import Store, who, brief
|
|
127
|
+
|
|
128
|
+
who("ada", field="aka")["value"] # ['Ada', 'Lovelace']
|
|
129
|
+
print(brief("ada-lovelace", purpose="ask")["text"])
|
|
130
|
+
|
|
131
|
+
store = Store() # MutableMapping[str, Entity] over a dol files store
|
|
132
|
+
entity = store[
|
|
133
|
+
"people/ada-lovelace"
|
|
134
|
+
] # a mapping of that person's files, plus parsed views
|
|
135
|
+
entity.identities, entity.rules, entity.sections["Write to them"]
|
|
136
|
+
Store(files={}) # any MutableMapping[str, str] of files: a dict, a remote store
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Design
|
|
140
|
+
|
|
141
|
+
The seams, surfaces and deliberate non-seams are in [Discussion #1](https://github.com/thorwhalen/acquaint/discussions/1).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""acquaint: people, and what they are involved in, for AI agents.
|
|
2
|
+
|
|
3
|
+
Who someone is, how to reach them, how to read them, how to write to them, kept as
|
|
4
|
+
hand-editable Markdown with a source on every preference, outside any code repository.
|
|
5
|
+
|
|
6
|
+
The verbs are the same in Python, on the command line (``acquaint who ada -f aka``) and
|
|
7
|
+
over MCP::
|
|
8
|
+
|
|
9
|
+
>>> from acquaint import new, remember, who, brief # doctest: +SKIP
|
|
10
|
+
>>> new("person", "Ada Lovelace") # doctest: +SKIP
|
|
11
|
+
>>> remember("ada-lovelace", "prefers email for anything with attachments",
|
|
12
|
+
... source="https://example.org/thread/1") # doctest: +SKIP
|
|
13
|
+
>>> who("ada", field="aka")["value"] # doctest: +SKIP
|
|
14
|
+
['Ada', 'Lovelace']
|
|
15
|
+
|
|
16
|
+
For library use, :class:`Store` is a ``MutableMapping`` of entities over any mapping of
|
|
17
|
+
files (a ``dol`` files store by default).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from acquaint.store import AcquaintError, Entity, Store, data_dir
|
|
21
|
+
from acquaint.tools import (
|
|
22
|
+
SIDE_EFFECTS,
|
|
23
|
+
TOOLS,
|
|
24
|
+
brief,
|
|
25
|
+
check,
|
|
26
|
+
forget,
|
|
27
|
+
lint,
|
|
28
|
+
new,
|
|
29
|
+
reach,
|
|
30
|
+
remember,
|
|
31
|
+
rename,
|
|
32
|
+
resolve,
|
|
33
|
+
style_lint,
|
|
34
|
+
sync_init,
|
|
35
|
+
sync_pull,
|
|
36
|
+
sync_push,
|
|
37
|
+
sync_status,
|
|
38
|
+
who,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AcquaintError",
|
|
43
|
+
"Entity",
|
|
44
|
+
"SIDE_EFFECTS",
|
|
45
|
+
"Store",
|
|
46
|
+
"TOOLS",
|
|
47
|
+
"brief",
|
|
48
|
+
"check",
|
|
49
|
+
"data_dir",
|
|
50
|
+
"forget",
|
|
51
|
+
"lint",
|
|
52
|
+
"new",
|
|
53
|
+
"reach",
|
|
54
|
+
"remember",
|
|
55
|
+
"rename",
|
|
56
|
+
"resolve",
|
|
57
|
+
"style_lint",
|
|
58
|
+
"sync_init",
|
|
59
|
+
"sync_pull",
|
|
60
|
+
"sync_push",
|
|
61
|
+
"sync_status",
|
|
62
|
+
"who",
|
|
63
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
2
|
+
"""``acquaint`` on the command line: ``cw`` over :data:`acquaint.tools.TOOLS`, with ``sync_*`` as a ``sync`` group.
|
|
3
|
+
|
|
4
|
+
``--json`` anywhere prints the tool's result dict instead of text; ``-`` as the text
|
|
5
|
+
of ``check`` or ``style-lint`` reads it from stdin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import cw
|
|
13
|
+
|
|
14
|
+
from acquaint import tools
|
|
15
|
+
from acquaint.render import render
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _command(func):
|
|
19
|
+
@functools.wraps(func)
|
|
20
|
+
def command(*args, **kwargs):
|
|
21
|
+
try:
|
|
22
|
+
return func(*args, **kwargs)
|
|
23
|
+
except (tools.AcquaintError, ValueError) as error:
|
|
24
|
+
raise cw.CommandError(str(error)) from error
|
|
25
|
+
|
|
26
|
+
return command
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _egress(as_json):
|
|
30
|
+
def egress(result, *, out, err):
|
|
31
|
+
if as_json:
|
|
32
|
+
print(json.dumps(result, indent=2, ensure_ascii=False), file=out)
|
|
33
|
+
return 0 if result.get("ok", True) else 1
|
|
34
|
+
stdout, stderr, code = render(result)
|
|
35
|
+
print(stderr, file=err) if stderr else None
|
|
36
|
+
print(stdout, file=out) if stdout else None
|
|
37
|
+
return code
|
|
38
|
+
|
|
39
|
+
return egress
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(argv=None):
|
|
43
|
+
for stream in (sys.stdout, sys.stderr): # a cp1252 pipe must not crash on "·" or "→"
|
|
44
|
+
getattr(stream, "reconfigure", lambda **_: None)(errors="backslashreplace")
|
|
45
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
46
|
+
as_json = "--json" in argv
|
|
47
|
+
commands = {
|
|
48
|
+
f.__name__.replace("_", "-"): _command(f)
|
|
49
|
+
for f in tools.TOOLS
|
|
50
|
+
if not f.__name__.startswith("sync_")
|
|
51
|
+
}
|
|
52
|
+
commands["sync"] = {
|
|
53
|
+
f.__name__.removeprefix("sync_"): _command(f)
|
|
54
|
+
for f in tools.TOOLS
|
|
55
|
+
if f.__name__.startswith("sync_")
|
|
56
|
+
}
|
|
57
|
+
stdin = {"text": {"codec": lambda text: sys.stdin.read() if text == "-" else text}}
|
|
58
|
+
config = {"check": stdin, "style-lint": stdin}
|
|
59
|
+
args = [a for a in argv if a != "--json"]
|
|
60
|
+
raise SystemExit(
|
|
61
|
+
cw.dispatch(
|
|
62
|
+
commands,
|
|
63
|
+
args,
|
|
64
|
+
prog="acquaint",
|
|
65
|
+
convention=cw.MODERN,
|
|
66
|
+
egress=_egress(as_json),
|
|
67
|
+
config=config,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|