epitype 1.0.0__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.
- epitype-1.0.0/LICENSE +21 -0
- epitype-1.0.0/PKG-INFO +167 -0
- epitype-1.0.0/README.md +152 -0
- epitype-1.0.0/adapters/__init__.py +1 -0
- epitype-1.0.0/adapters/claude/__init__.py +8 -0
- epitype-1.0.0/adapters/claude/_hook_common.py +188 -0
- epitype-1.0.0/adapters/claude/precompact_hook.py +163 -0
- epitype-1.0.0/adapters/claude/pretooluse_gate.py +868 -0
- epitype-1.0.0/adapters/claude/recall_hook.py +871 -0
- epitype-1.0.0/adapters/claude/sessionstart_hook.py +180 -0
- epitype-1.0.0/adapters/codex/__init__.py +1 -0
- epitype-1.0.0/adapters/codex/config_guard.py +316 -0
- epitype-1.0.0/adapters/codex/hook_trust.py +270 -0
- epitype-1.0.0/adapters/codex/hooks_template.json +50 -0
- epitype-1.0.0/adapters/shim_template.py +124 -0
- epitype-1.0.0/epitype/__init__.py +3 -0
- epitype-1.0.0/epitype/compact_map.py +377 -0
- epitype-1.0.0/epitype/decision_lint.py +689 -0
- epitype-1.0.0/epitype/ledger_gate.py +400 -0
- epitype-1.0.0/epitype/memsearch.py +1668 -0
- epitype-1.0.0/epitype/memspec.py +447 -0
- epitype-1.0.0/epitype/scar_census.py +918 -0
- epitype-1.0.0/epitype/token_meter.py +208 -0
- epitype-1.0.0/epitype.egg-info/PKG-INFO +167 -0
- epitype-1.0.0/epitype.egg-info/SOURCES.txt +34 -0
- epitype-1.0.0/epitype.egg-info/dependency_links.txt +1 -0
- epitype-1.0.0/epitype.egg-info/entry_points.txt +2 -0
- epitype-1.0.0/epitype.egg-info/top_level.txt +4 -0
- epitype-1.0.0/exam/__init__.py +1 -0
- epitype-1.0.0/exam/exam_runner.py +319 -0
- epitype-1.0.0/exam/sample_corpus.json +249 -0
- epitype-1.0.0/install/__init__.py +1 -0
- epitype-1.0.0/install/graft.py +1930 -0
- epitype-1.0.0/install/scar_scan.py +281 -0
- epitype-1.0.0/pyproject.toml +42 -0
- epitype-1.0.0/setup.cfg +4 -0
epitype-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Epitype contributors
|
|
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.
|
epitype-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epitype
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Epitype is a memory governance layer for CLI agents.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/leavemagic-cyber/epitype
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Epitype
|
|
17
|
+
|
|
18
|
+
[繁體中文](README.zh-TW.md)
|
|
19
|
+
|
|
20
|
+
Epitype is a memory governance layer for CLI agents.
|
|
21
|
+
|
|
22
|
+
It keeps the host's native memory as the storage authority, then adds the structure, timing, action gates, and evidence needed for remembered rules to affect later behavior.
|
|
23
|
+
|
|
24
|
+
An agent can retrieve the right fact and still break the rule attached to it. Epitype focuses on that gap:
|
|
25
|
+
|
|
26
|
+
- a replaced decision should not return as current;
|
|
27
|
+
- an incident lesson should reach the tool action where it matters;
|
|
28
|
+
- a permission should remain attributable to the person who gave it;
|
|
29
|
+
- a memory failure should be visible and testable.
|
|
30
|
+
|
|
31
|
+
Epitype currently supports Claude Code and Codex. It uses only the Python standard library and does not require a hosted memory service.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
Epitype connects the same native vaults to four host events:
|
|
36
|
+
|
|
37
|
+
| Event | What Epitype does |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `SessionStart` | Injects a bounded memory index and work ledger when those files exist. |
|
|
40
|
+
| `UserPromptSubmit` | Recalls up to five relevant cards from each resolved vault within the shared output budget. Short owner-grant statements are stored verbatim, deduplicated, and indexed; their meaning is not inferred during capture. |
|
|
41
|
+
| `PreToolUse` | Matches scar-card triggers against the tool and its input. A match returns a bounded denial, safer advice, and an audit row. |
|
|
42
|
+
| `PreCompact` | Builds a small recovery map from the transcript tail before context compaction. |
|
|
43
|
+
|
|
44
|
+
Injected memory remains advisory. It cannot override system or developer instructions, bypass host permissions, or grant a tool authority by itself. Hook output is capped at 10 KiB and each hook has a three-second fail-open deadline.
|
|
45
|
+
|
|
46
|
+
## Governance beyond recall
|
|
47
|
+
|
|
48
|
+
### Current decisions
|
|
49
|
+
|
|
50
|
+
Decision cards have a stable `decision_key`, an `active` or `superseded` status, an effective time, and a named decision source. Exactly one card should be active for each key. `query`, `recall`, and the prompt hook exclude superseded cards by default while retaining them for provenance. Use `--include-superseded` only when you want the history.
|
|
51
|
+
|
|
52
|
+
### Scars that can stop an action
|
|
53
|
+
|
|
54
|
+
A scar is an incident-born rule. Adding `trigger.tool`, `trigger.input`, and actionable `advice` turns a suitable scar into a narrow action gate. Command matching inspects executable and unquoted argument positions by default, so a trigger word inside a quoted string, comment, or heredoc body does not block the command. Cards that need literal full-text matching can opt in explicitly.
|
|
55
|
+
|
|
56
|
+
### Native-first installation
|
|
57
|
+
|
|
58
|
+
The installer merges only entries marked as Epitype, keeps detected native vaults, and writes backups before changing an existing host file. Stable shims let the repository move without rewriting every host registration. Uninstall removes Epitype-owned registrations and configuration while preserving native memory and vault cards.
|
|
59
|
+
|
|
60
|
+
### Failure evidence
|
|
61
|
+
|
|
62
|
+
Missing indexes, stale indexes, shim failures, malformed cards, and lock contention have distinct outcomes. The hooks fail open when they cannot safely finish, and the installer doctor reports recorded shim outages instead of treating silence as health.
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
Requirements: Python 3.11 or newer and a Claude Code or Codex installation with hook support.
|
|
67
|
+
|
|
68
|
+
Install: `pip install epitype`, then use the `epitype-graft` command in place of `python install/graft.py` below (for example `epitype-graft install --dry-run`).
|
|
69
|
+
|
|
70
|
+
The `epitype` package on npm is only a signpost back to this Python project.
|
|
71
|
+
|
|
72
|
+
From the repository root, preview the planned changes:
|
|
73
|
+
|
|
74
|
+
```powershell
|
|
75
|
+
python install/graft.py install --dry-run
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If the preview contains only the hosts and paths you expect, install and run the synthetic health check:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
python install/graft.py install
|
|
82
|
+
python install/graft.py doctor
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The installer detects existing native vaults. If it finds none, it creates an empty fallback vault. Reinstall preserves a curated vault list; use `python install/graft.py vaults --resync --dry-run` and then rerun without `--dry-run` when you intentionally want to adopt the latest detection result.
|
|
86
|
+
|
|
87
|
+
### Approve Codex hooks
|
|
88
|
+
|
|
89
|
+
Codex registration and Codex trust are separate. Check the real trust state after installation:
|
|
90
|
+
|
|
91
|
+
```powershell
|
|
92
|
+
python adapters/codex/hook_trust.py check
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If any Epitype entry is `UNTRUSTED`, `DISABLED`, or `MODIFIED`:
|
|
96
|
+
|
|
97
|
+
- In the terminal UI, enter `/hooks`, press `t` to trust all entries in the panel, then press `esc`.
|
|
98
|
+
- In the Desktop app, open **hooks need review** or the **Hooks** panel and approve the Epitype entries for `SessionStart`, `UserPromptSubmit`, `PreToolUse`, and `PreCompact`.
|
|
99
|
+
|
|
100
|
+
Run the check again. Codex is ready only when it prints `CODEX TRUST: PASS 4/4`. `doctor` verifies registration and synthetic execution; it does not replace this trust check.
|
|
101
|
+
|
|
102
|
+
### Choose a vault layout
|
|
103
|
+
|
|
104
|
+
Start from one of the tracked templates:
|
|
105
|
+
|
|
106
|
+
| Template | Intended use |
|
|
107
|
+
|---|---|
|
|
108
|
+
| [`minimal`](templates/minimal/) | One person on one machine. |
|
|
109
|
+
| [`team`](templates/team/) | A shared vault using the common write-lock contract. |
|
|
110
|
+
| [`power`](templates/power/) | The full layout, including census and exam-ready directories. |
|
|
111
|
+
|
|
112
|
+
## Search the local vault
|
|
113
|
+
|
|
114
|
+
Build a vault's local FTS index, then query it directly or recall against a prompt:
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
python epitype/memsearch.py build C:\path\to\vault
|
|
118
|
+
python epitype/memsearch.py query term --vault C:\path\to\vault
|
|
119
|
+
python epitype/memsearch.py recall "natural-language prompt" --vault C:\path\to\vault
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The generated database lives at `<vault>/.epitype/memory_fts.sqlite3` and is ignored by Git. Only `build` creates a missing index. Existing indexes refresh incrementally when stale; a missing index is reported separately from a valid zero-result query.
|
|
123
|
+
|
|
124
|
+
## Verify this checkout
|
|
125
|
+
|
|
126
|
+
Run the public checks from the repository root:
|
|
127
|
+
|
|
128
|
+
```powershell
|
|
129
|
+
python tests/run_all.py
|
|
130
|
+
python tests/privacy_lint.py
|
|
131
|
+
python exam/exam_runner.py --strict
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`tests/run_all.py` currently runs 17 component selftests covering the core tools, hook adapters, installer, exam engine, and privacy gate. The included exam corpus is a small synthetic sample. For this release, the publication gate also passed a strict 300-case behavior corpus and a 15-seed review; those release materials are not part of this repository.
|
|
135
|
+
|
|
136
|
+
These checks are regression evidence, not proof that every future host version or every memory failure is covered.
|
|
137
|
+
|
|
138
|
+
## Moving or removing Epitype
|
|
139
|
+
|
|
140
|
+
After moving the repository, update the stable shim target and rerun the doctor:
|
|
141
|
+
|
|
142
|
+
```powershell
|
|
143
|
+
python install/graft.py relocate --to C:\path\to\new\repo
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Preview uninstall before removing Epitype-owned files:
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
python install/graft.py uninstall --dry-run
|
|
150
|
+
python install/graft.py uninstall
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Read [Uninstall Epitype](docs/UNINSTALL.md) before restoring a backup manually.
|
|
154
|
+
|
|
155
|
+
## Limits
|
|
156
|
+
|
|
157
|
+
- Hooks can govern only events and tools the host exposes. Direct file reads remain outside Epitype's current-decision filter.
|
|
158
|
+
- The time and output ceilings require selection; Epitype never injects the entire vault into every prompt.
|
|
159
|
+
- Action gates are only as precise as their scar triggers and advice. Malformed cards fail open rather than taking control of the host.
|
|
160
|
+
- Claude Code and Codex are the tested host boundary. A host upgrade still needs integration testing.
|
|
161
|
+
- The bundled tests are synthetic. They exercise behavior and failure handling, not long-term field performance.
|
|
162
|
+
|
|
163
|
+
## Documentation
|
|
164
|
+
|
|
165
|
+
- [Architecture](docs/ARCHITECTURE.md): memory blocks, retrieval routes, decision cards, scar lifecycle, and authority rules.
|
|
166
|
+
- [Failure modes](docs/FAILURE_MODES.md): symptoms, countermeasures, and verification boundaries.
|
|
167
|
+
- [Uninstall](docs/UNINSTALL.md): ownership-aware removal and backup guidance.
|
epitype-1.0.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Epitype
|
|
2
|
+
|
|
3
|
+
[繁體中文](README.zh-TW.md)
|
|
4
|
+
|
|
5
|
+
Epitype is a memory governance layer for CLI agents.
|
|
6
|
+
|
|
7
|
+
It keeps the host's native memory as the storage authority, then adds the structure, timing, action gates, and evidence needed for remembered rules to affect later behavior.
|
|
8
|
+
|
|
9
|
+
An agent can retrieve the right fact and still break the rule attached to it. Epitype focuses on that gap:
|
|
10
|
+
|
|
11
|
+
- a replaced decision should not return as current;
|
|
12
|
+
- an incident lesson should reach the tool action where it matters;
|
|
13
|
+
- a permission should remain attributable to the person who gave it;
|
|
14
|
+
- a memory failure should be visible and testable.
|
|
15
|
+
|
|
16
|
+
Epitype currently supports Claude Code and Codex. It uses only the Python standard library and does not require a hosted memory service.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
Epitype connects the same native vaults to four host events:
|
|
21
|
+
|
|
22
|
+
| Event | What Epitype does |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `SessionStart` | Injects a bounded memory index and work ledger when those files exist. |
|
|
25
|
+
| `UserPromptSubmit` | Recalls up to five relevant cards from each resolved vault within the shared output budget. Short owner-grant statements are stored verbatim, deduplicated, and indexed; their meaning is not inferred during capture. |
|
|
26
|
+
| `PreToolUse` | Matches scar-card triggers against the tool and its input. A match returns a bounded denial, safer advice, and an audit row. |
|
|
27
|
+
| `PreCompact` | Builds a small recovery map from the transcript tail before context compaction. |
|
|
28
|
+
|
|
29
|
+
Injected memory remains advisory. It cannot override system or developer instructions, bypass host permissions, or grant a tool authority by itself. Hook output is capped at 10 KiB and each hook has a three-second fail-open deadline.
|
|
30
|
+
|
|
31
|
+
## Governance beyond recall
|
|
32
|
+
|
|
33
|
+
### Current decisions
|
|
34
|
+
|
|
35
|
+
Decision cards have a stable `decision_key`, an `active` or `superseded` status, an effective time, and a named decision source. Exactly one card should be active for each key. `query`, `recall`, and the prompt hook exclude superseded cards by default while retaining them for provenance. Use `--include-superseded` only when you want the history.
|
|
36
|
+
|
|
37
|
+
### Scars that can stop an action
|
|
38
|
+
|
|
39
|
+
A scar is an incident-born rule. Adding `trigger.tool`, `trigger.input`, and actionable `advice` turns a suitable scar into a narrow action gate. Command matching inspects executable and unquoted argument positions by default, so a trigger word inside a quoted string, comment, or heredoc body does not block the command. Cards that need literal full-text matching can opt in explicitly.
|
|
40
|
+
|
|
41
|
+
### Native-first installation
|
|
42
|
+
|
|
43
|
+
The installer merges only entries marked as Epitype, keeps detected native vaults, and writes backups before changing an existing host file. Stable shims let the repository move without rewriting every host registration. Uninstall removes Epitype-owned registrations and configuration while preserving native memory and vault cards.
|
|
44
|
+
|
|
45
|
+
### Failure evidence
|
|
46
|
+
|
|
47
|
+
Missing indexes, stale indexes, shim failures, malformed cards, and lock contention have distinct outcomes. The hooks fail open when they cannot safely finish, and the installer doctor reports recorded shim outages instead of treating silence as health.
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
Requirements: Python 3.11 or newer and a Claude Code or Codex installation with hook support.
|
|
52
|
+
|
|
53
|
+
Install: `pip install epitype`, then use the `epitype-graft` command in place of `python install/graft.py` below (for example `epitype-graft install --dry-run`).
|
|
54
|
+
|
|
55
|
+
The `epitype` package on npm is only a signpost back to this Python project.
|
|
56
|
+
|
|
57
|
+
From the repository root, preview the planned changes:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
python install/graft.py install --dry-run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If the preview contains only the hosts and paths you expect, install and run the synthetic health check:
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
python install/graft.py install
|
|
67
|
+
python install/graft.py doctor
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The installer detects existing native vaults. If it finds none, it creates an empty fallback vault. Reinstall preserves a curated vault list; use `python install/graft.py vaults --resync --dry-run` and then rerun without `--dry-run` when you intentionally want to adopt the latest detection result.
|
|
71
|
+
|
|
72
|
+
### Approve Codex hooks
|
|
73
|
+
|
|
74
|
+
Codex registration and Codex trust are separate. Check the real trust state after installation:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
python adapters/codex/hook_trust.py check
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If any Epitype entry is `UNTRUSTED`, `DISABLED`, or `MODIFIED`:
|
|
81
|
+
|
|
82
|
+
- In the terminal UI, enter `/hooks`, press `t` to trust all entries in the panel, then press `esc`.
|
|
83
|
+
- In the Desktop app, open **hooks need review** or the **Hooks** panel and approve the Epitype entries for `SessionStart`, `UserPromptSubmit`, `PreToolUse`, and `PreCompact`.
|
|
84
|
+
|
|
85
|
+
Run the check again. Codex is ready only when it prints `CODEX TRUST: PASS 4/4`. `doctor` verifies registration and synthetic execution; it does not replace this trust check.
|
|
86
|
+
|
|
87
|
+
### Choose a vault layout
|
|
88
|
+
|
|
89
|
+
Start from one of the tracked templates:
|
|
90
|
+
|
|
91
|
+
| Template | Intended use |
|
|
92
|
+
|---|---|
|
|
93
|
+
| [`minimal`](templates/minimal/) | One person on one machine. |
|
|
94
|
+
| [`team`](templates/team/) | A shared vault using the common write-lock contract. |
|
|
95
|
+
| [`power`](templates/power/) | The full layout, including census and exam-ready directories. |
|
|
96
|
+
|
|
97
|
+
## Search the local vault
|
|
98
|
+
|
|
99
|
+
Build a vault's local FTS index, then query it directly or recall against a prompt:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
python epitype/memsearch.py build C:\path\to\vault
|
|
103
|
+
python epitype/memsearch.py query term --vault C:\path\to\vault
|
|
104
|
+
python epitype/memsearch.py recall "natural-language prompt" --vault C:\path\to\vault
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The generated database lives at `<vault>/.epitype/memory_fts.sqlite3` and is ignored by Git. Only `build` creates a missing index. Existing indexes refresh incrementally when stale; a missing index is reported separately from a valid zero-result query.
|
|
108
|
+
|
|
109
|
+
## Verify this checkout
|
|
110
|
+
|
|
111
|
+
Run the public checks from the repository root:
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
python tests/run_all.py
|
|
115
|
+
python tests/privacy_lint.py
|
|
116
|
+
python exam/exam_runner.py --strict
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`tests/run_all.py` currently runs 17 component selftests covering the core tools, hook adapters, installer, exam engine, and privacy gate. The included exam corpus is a small synthetic sample. For this release, the publication gate also passed a strict 300-case behavior corpus and a 15-seed review; those release materials are not part of this repository.
|
|
120
|
+
|
|
121
|
+
These checks are regression evidence, not proof that every future host version or every memory failure is covered.
|
|
122
|
+
|
|
123
|
+
## Moving or removing Epitype
|
|
124
|
+
|
|
125
|
+
After moving the repository, update the stable shim target and rerun the doctor:
|
|
126
|
+
|
|
127
|
+
```powershell
|
|
128
|
+
python install/graft.py relocate --to C:\path\to\new\repo
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Preview uninstall before removing Epitype-owned files:
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
python install/graft.py uninstall --dry-run
|
|
135
|
+
python install/graft.py uninstall
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Read [Uninstall Epitype](docs/UNINSTALL.md) before restoring a backup manually.
|
|
139
|
+
|
|
140
|
+
## Limits
|
|
141
|
+
|
|
142
|
+
- Hooks can govern only events and tools the host exposes. Direct file reads remain outside Epitype's current-decision filter.
|
|
143
|
+
- The time and output ceilings require selection; Epitype never injects the entire vault into every prompt.
|
|
144
|
+
- Action gates are only as precise as their scar triggers and advice. Malformed cards fail open rather than taking control of the host.
|
|
145
|
+
- Claude Code and Codex are the tested host boundary. A host upgrade still needs integration testing.
|
|
146
|
+
- The bundled tests are synthetic. They exercise behavior and failure handling, not long-term field performance.
|
|
147
|
+
|
|
148
|
+
## Documentation
|
|
149
|
+
|
|
150
|
+
- [Architecture](docs/ARCHITECTURE.md): memory blocks, retrieval routes, decision cards, scar lifecycle, and authority rules.
|
|
151
|
+
- [Failure modes](docs/FAILURE_MODES.md): symptoms, countermeasures, and verification boundaries.
|
|
152
|
+
- [Uninstall](docs/UNINSTALL.md): ownership-aware removal and backup guidance.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import sys; sys.dont_write_bytecode = True; [getattr(stream, "reconfigure", lambda **_: None)(encoding="utf-8", errors="replace") for stream in (sys.stdout, sys.stderr)] # cp950 consoles must not break hook entrypoints.
|
|
2
|
+
"""Shared fail-open mechanics for Claude hook adapters."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from epitype import memspec
|
|
11
|
+
|
|
12
|
+
NATIVE_PROJECTS_SUBPATH = (".claude", "projects")
|
|
13
|
+
_SLUG_PATTERN = re.compile(r"[^A-Za-z0-9]")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def expired(started_at):
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
return time.monotonic() - started_at >= memspec.HOOK_TIMEOUT_SECONDS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_event(stream):
|
|
23
|
+
value = json.load(stream)
|
|
24
|
+
if not isinstance(value, dict):
|
|
25
|
+
raise ValueError("hook input must be a JSON object")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_config(started_at):
|
|
30
|
+
if expired(started_at):
|
|
31
|
+
return None
|
|
32
|
+
configured = os.environ.get(memspec.EPITYPE_CONFIG_ENV)
|
|
33
|
+
path = (
|
|
34
|
+
Path(configured).expanduser()
|
|
35
|
+
if configured
|
|
36
|
+
else Path.home() / ".epitype" / "config.json"
|
|
37
|
+
)
|
|
38
|
+
raw = path.read_text(encoding="utf-8")
|
|
39
|
+
value = json.loads(raw)
|
|
40
|
+
if not isinstance(value, dict):
|
|
41
|
+
raise ValueError("config must be an object")
|
|
42
|
+
|
|
43
|
+
raw_vaults = value.get(memspec.CONFIG_VAULTS_FIELD)
|
|
44
|
+
if not isinstance(raw_vaults, list) or not raw_vaults:
|
|
45
|
+
raise ValueError("config vaults must be a non-empty list")
|
|
46
|
+
vaults = []
|
|
47
|
+
for item in raw_vaults:
|
|
48
|
+
if not isinstance(item, str) or not item.strip():
|
|
49
|
+
raise ValueError("vault paths must be non-empty strings")
|
|
50
|
+
vault = Path(item).expanduser().resolve()
|
|
51
|
+
if not vault.is_dir():
|
|
52
|
+
raise NotADirectoryError(str(vault))
|
|
53
|
+
vaults.append(vault)
|
|
54
|
+
|
|
55
|
+
raw_budget = value.get(
|
|
56
|
+
memspec.CONFIG_BUDGET_BYTES_FIELD,
|
|
57
|
+
memspec.HOOK_DEFAULT_BUDGET_BYTES,
|
|
58
|
+
)
|
|
59
|
+
if isinstance(raw_budget, bool) or not isinstance(raw_budget, int):
|
|
60
|
+
raise ValueError("budget_bytes must be a positive integer")
|
|
61
|
+
budget = raw_budget
|
|
62
|
+
if budget <= 0:
|
|
63
|
+
raise ValueError("budget_bytes must be a positive integer")
|
|
64
|
+
return {
|
|
65
|
+
memspec.CONFIG_VAULTS_FIELD: vaults,
|
|
66
|
+
memspec.CONFIG_BUDGET_BYTES_FIELD: min(
|
|
67
|
+
budget,
|
|
68
|
+
memspec.HOOK_DEFAULT_BUDGET_BYTES,
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _holds_cards(vault):
|
|
74
|
+
try:
|
|
75
|
+
if (vault / memspec.MEMORY_INDEX_FILENAME).is_file():
|
|
76
|
+
return True
|
|
77
|
+
return any(
|
|
78
|
+
item.suffix.lower() == ".md" and not item.name.startswith("_")
|
|
79
|
+
for item in vault.iterdir()
|
|
80
|
+
)
|
|
81
|
+
except OSError:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def native_cwd_vaults(cwd, home=None):
|
|
86
|
+
"""Claude Code auto-creates one memory directory per cwd slug; cards written
|
|
87
|
+
there must be recallable without editing config, so the cwd and each ancestor
|
|
88
|
+
join the vault list whenever their directory already holds an index or a card.
|
|
89
|
+
Empty auto-created shells are skipped so no index is planted in them."""
|
|
90
|
+
if not isinstance(cwd, str) or not cwd.strip():
|
|
91
|
+
return []
|
|
92
|
+
projects = (home or Path.home()).joinpath(*NATIVE_PROJECTS_SUBPATH)
|
|
93
|
+
try:
|
|
94
|
+
start = Path(cwd)
|
|
95
|
+
bases = (start, *start.parents)
|
|
96
|
+
except (TypeError, ValueError):
|
|
97
|
+
return []
|
|
98
|
+
found = []
|
|
99
|
+
for base in bases:
|
|
100
|
+
texts = {str(base)}
|
|
101
|
+
try:
|
|
102
|
+
texts.add(str(base.resolve()))
|
|
103
|
+
except OSError:
|
|
104
|
+
pass
|
|
105
|
+
for text in sorted(texts):
|
|
106
|
+
candidate = projects / _SLUG_PATTERN.sub("-", text) / "memory"
|
|
107
|
+
if candidate.is_dir() and _holds_cards(candidate):
|
|
108
|
+
resolved = candidate.resolve()
|
|
109
|
+
if resolved not in found:
|
|
110
|
+
found.append(resolved)
|
|
111
|
+
return found
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def resolve_vaults(config, event, home=None):
|
|
115
|
+
"""Closest native cwd vault first, then the configured vaults, deduplicated."""
|
|
116
|
+
vaults = native_cwd_vaults(event.get("cwd") if isinstance(event, dict) else None, home)
|
|
117
|
+
for vault in config[memspec.CONFIG_VAULTS_FIELD]:
|
|
118
|
+
if vault not in vaults:
|
|
119
|
+
vaults.append(vault)
|
|
120
|
+
return vaults
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def payload(event_name, context):
|
|
124
|
+
return {
|
|
125
|
+
"hookSpecificOutput": {
|
|
126
|
+
"hookEventName": event_name,
|
|
127
|
+
"additionalContext": context,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def encode_payload(value):
|
|
133
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def payload_fits(event_name, context, context_budget):
|
|
137
|
+
if len(context.encode("utf-8")) > context_budget:
|
|
138
|
+
return False
|
|
139
|
+
encoded = encode_payload(payload(event_name, context)).encode("utf-8")
|
|
140
|
+
return len(encoded) <= memspec.HOOK_MAX_OUTPUT_BYTES
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def bounded_context(event_name, pieces, context_budget, required_first=False):
|
|
144
|
+
selected = []
|
|
145
|
+
for piece in pieces:
|
|
146
|
+
if not isinstance(piece, str) or not piece:
|
|
147
|
+
continue
|
|
148
|
+
candidate = "\n".join(selected + [piece])
|
|
149
|
+
if payload_fits(event_name, candidate, context_budget):
|
|
150
|
+
selected.append(piece)
|
|
151
|
+
elif required_first and not selected:
|
|
152
|
+
return None
|
|
153
|
+
return "\n".join(selected) if selected else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def emit(value):
|
|
157
|
+
encoded = encode_payload(value)
|
|
158
|
+
if len(encoded.encode("utf-8")) > memspec.HOOK_MAX_OUTPUT_BYTES:
|
|
159
|
+
raise ValueError("hook output exceeds the hard byte limit")
|
|
160
|
+
print(encoded)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_synthetic(script, event, config_path, arguments=(), environment=None):
|
|
164
|
+
environment = {**os.environ, **(environment or {})}
|
|
165
|
+
environment[memspec.EPITYPE_CONFIG_ENV] = os.fspath(config_path)
|
|
166
|
+
environment["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
167
|
+
return subprocess.run(
|
|
168
|
+
[sys.executable, os.fspath(script), *arguments],
|
|
169
|
+
input=json.dumps(event, ensure_ascii=False),
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
encoding="utf-8",
|
|
173
|
+
errors="replace",
|
|
174
|
+
env=environment,
|
|
175
|
+
timeout=10,
|
|
176
|
+
check=False,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def write_config(path, vaults, budget=memspec.HOOK_DEFAULT_BUDGET_BYTES):
|
|
181
|
+
value = {
|
|
182
|
+
memspec.CONFIG_VAULTS_FIELD: [os.fspath(item) for item in vaults],
|
|
183
|
+
memspec.CONFIG_BUDGET_BYTES_FIELD: budget,
|
|
184
|
+
}
|
|
185
|
+
path.write_text(
|
|
186
|
+
json.dumps(value, ensure_ascii=False),
|
|
187
|
+
encoding="utf-8",
|
|
188
|
+
)
|