daimon-briefing 0.3.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.
- daimon_briefing-0.3.0/.gitignore +4 -0
- daimon_briefing-0.3.0/PKG-INFO +161 -0
- daimon_briefing-0.3.0/README.md +148 -0
- daimon_briefing-0.3.0/daimon_briefing/__init__.py +25 -0
- daimon_briefing-0.3.0/daimon_briefing/anchor.py +103 -0
- daimon_briefing-0.3.0/daimon_briefing/briefing.py +274 -0
- daimon_briefing-0.3.0/daimon_briefing/carry.py +97 -0
- daimon_briefing-0.3.0/daimon_briefing/cli.py +1119 -0
- daimon_briefing-0.3.0/daimon_briefing/config.py +406 -0
- daimon_briefing-0.3.0/daimon_briefing/configure.py +81 -0
- daimon_briefing-0.3.0/daimon_briefing/harvest.py +189 -0
- daimon_briefing-0.3.0/daimon_briefing/hooks.py +78 -0
- daimon_briefing-0.3.0/daimon_briefing/llm.py +239 -0
- daimon_briefing-0.3.0/daimon_briefing/recall.py +588 -0
- daimon_briefing-0.3.0/daimon_briefing/render.py +389 -0
- daimon_briefing-0.3.0/daimon_briefing/scoring.py +79 -0
- daimon_briefing-0.3.0/daimon_briefing/serializer.py +550 -0
- daimon_briefing-0.3.0/daimon_briefing/store.py +506 -0
- daimon_briefing-0.3.0/daimon_briefing/teamsync.py +484 -0
- daimon_briefing-0.3.0/daimon_briefing/transcript.py +258 -0
- daimon_briefing-0.3.0/pyproject.toml +33 -0
- daimon_briefing-0.3.0/skills/daimon-briefing/SKILL.md +49 -0
- daimon_briefing-0.3.0/skills/daimon-end/SKILL.md +74 -0
- daimon_briefing-0.3.0/tests/__init__.py +0 -0
- daimon_briefing-0.3.0/tests/conftest.py +119 -0
- daimon_briefing-0.3.0/tests/fixtures/sample_transcript.md +18 -0
- daimon_briefing-0.3.0/tests/test_anchor.py +137 -0
- daimon_briefing-0.3.0/tests/test_briefing.py +448 -0
- daimon_briefing-0.3.0/tests/test_carry.py +147 -0
- daimon_briefing-0.3.0/tests/test_claude_hooks.py +506 -0
- daimon_briefing-0.3.0/tests/test_cli.py +2321 -0
- daimon_briefing-0.3.0/tests/test_codex_hooks.py +330 -0
- daimon_briefing-0.3.0/tests/test_config.py +403 -0
- daimon_briefing-0.3.0/tests/test_configure.py +153 -0
- daimon_briefing-0.3.0/tests/test_gemini_hooks.py +372 -0
- daimon_briefing-0.3.0/tests/test_harvest.py +241 -0
- daimon_briefing-0.3.0/tests/test_hooks.py +344 -0
- daimon_briefing-0.3.0/tests/test_isolation.py +36 -0
- daimon_briefing-0.3.0/tests/test_llm.py +361 -0
- daimon_briefing-0.3.0/tests/test_recall.py +625 -0
- daimon_briefing-0.3.0/tests/test_render.py +486 -0
- daimon_briefing-0.3.0/tests/test_scoring.py +79 -0
- daimon_briefing-0.3.0/tests/test_serializer.py +866 -0
- daimon_briefing-0.3.0/tests/test_store.py +751 -0
- daimon_briefing-0.3.0/tests/test_teamsync.py +473 -0
- daimon_briefing-0.3.0/tests/test_transcript.py +215 -0
- daimon_briefing-0.3.0/uv.lock +195 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: daimon-briefing
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Dream-briefing hermes plugin: cognitive checkpoint at session end, 'while you were away' briefing at session start. Slice 1 (local-file, no Honcho).
|
|
5
|
+
Author: Daily-Nerd / Daimon
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
10
|
+
Provides-Extra: pretty
|
|
11
|
+
Requires-Dist: rich>=13; extra == 'pretty'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Daimon Dream-Briefing — hermes plugin (Slice 2)
|
|
15
|
+
|
|
16
|
+
A **dream-briefing** is a session-*start* artifact: a skimmable "while you were
|
|
17
|
+
away / here's where we left off" briefing the agent shows you when you resume work,
|
|
18
|
+
reconstructed from a cognitive checkpoint written at the end of the prior session.
|
|
19
|
+
|
|
20
|
+
This is **Slice 2**: local-file checkpoints, no Honcho. Serialization is
|
|
21
|
+
single-pass for short sessions and chunked multi-pass (armC: per-chunk D-007
|
|
22
|
+
serialize → 01c merge with Q-STALE latest-state preference) above
|
|
23
|
+
`DAIMON_CHUNK_LINES` rendered lines. Failures are named (`SerializeError`
|
|
24
|
+
subclasses) so the CLI and logs say what actually broke.
|
|
25
|
+
Dogfoodable in hermes immediately, and runnable standalone on a plain transcript
|
|
26
|
+
file via the CLI (no hermes required).
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
SESSION N ── on_session_end ──► read transcript (SessionDB)
|
|
32
|
+
└► serialize (D-007 prompt + LLM) → validate → ~/.daimon/checkpoints/<id>.json
|
|
33
|
+
|
|
34
|
+
SESSION N+1 ── first pre_llm_call ──► load latest checkpoint
|
|
35
|
+
└► render briefing (deterministic template)
|
|
36
|
+
└► return {"context": briefing} → appended to your first user message
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The briefing puts **open loops first**, flags items whose state may have changed
|
|
40
|
+
**outside the AI session** (the PR-merge gap) with a *verify before trusting* marker,
|
|
41
|
+
then lists decisions and beliefs. Verbatim (extractively-pinned, D-006) facts are
|
|
42
|
+
marked distinctly from inferred ones.
|
|
43
|
+
|
|
44
|
+
## Install (in hermes)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# From a published repo / package:
|
|
48
|
+
hermes plugins install owner/daimon-plugin --enable
|
|
49
|
+
|
|
50
|
+
# Local editable (development):
|
|
51
|
+
uv pip install -e . # registers the hermes_agent.plugins entry point
|
|
52
|
+
# or copy this directory into ~/.hermes/plugins/daimon-briefing/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The plugin registers two hooks (`on_session_end`, `pre_llm_call`) and bundles the
|
|
56
|
+
user-facing skill as `daimon-briefing:daimon-briefing` (loadable via
|
|
57
|
+
`skill_view("daimon-briefing:daimon-briefing")`).
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
All config is via environment variables. `DAIMON_*` takes precedence; LLM settings
|
|
62
|
+
fall back to the Track-A `LITELLM_*` vars.
|
|
63
|
+
|
|
64
|
+
Every variable also resolves from `~/.daimon/env` when absent from the process
|
|
65
|
+
environment (process env always wins; override the file location with
|
|
66
|
+
`DAIMON_ENV_FILE`). This is how hooks get credentials: a hook inherits whatever
|
|
67
|
+
environment the host process was launched with — a GUI-launched Claude Code has
|
|
68
|
+
no shell profile — so shell exports are not a reliable channel. Format is plain
|
|
69
|
+
`KEY=VALUE` lines (`export ` prefix, quotes, and `#` comments tolerated):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# ~/.daimon/env — chmod 600, it holds API keys
|
|
73
|
+
DAIMON_LLM_API_KEY=sk-...
|
|
74
|
+
DAIMON_LLM_MODEL=<model-name>
|
|
75
|
+
DAIMON_LLM_BASE_URL=http://localhost:4000
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Variable | Default | Purpose |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `DAIMON_DISABLE` | (unset) | `1` = kill switch; hooks become no-ops |
|
|
81
|
+
| `DAIMON_CHECKPOINT_DIR` | `~/.daimon/checkpoints` | Where checkpoints + `latest.json` live |
|
|
82
|
+
| `DAIMON_LOG_DIR` | `~/.daimon/logs` | Where `status` looks for `serialize.log` (the session-end hook writes there) |
|
|
83
|
+
| `DAIMON_PROJECT_DIR` | (unset) | Working directory of the session, for per-project routing. When set, `serialize` also writes `<checkpoint-dir>/<project-slug>/latest.json` and `brief` prefers it (falling back to the global `latest.json`). The Claude Code hooks set this from the payload `cwd`; unset = project unknown = global-only behavior |
|
|
84
|
+
| `DAIMON_MIN_MESSAGES` | `10` | Skip serialization for sessions shorter than this |
|
|
85
|
+
| `DAIMON_TIMEOUT` | `120` | TOTAL budget (seconds) for the session-end serialize LLM work. A deadline is computed at hook start and shared across all retry attempts: per-attempt socket timeouts are capped to the remaining budget, and retries stop when it is exhausted |
|
|
86
|
+
| `DAIMON_CHUNK_LINES` | `1200` | Rendered-transcript line count above which serialization goes chunked (armC: per-chunk serialize → merge). 1200 matches the D-007 recall cliff |
|
|
87
|
+
| `DAIMON_CHUNK_OVERLAP` | `100` | Lines shared between consecutive chunks so boundary decisions aren't lost |
|
|
88
|
+
| `DAIMON_CHUNK_CONCURRENCY` | `4` | Parallel chunk-serialize calls. Gateway calls are generation-bound (~minutes each); sequential chunking made long sessions take chunk-count × minutes |
|
|
89
|
+
| `DAIMON_LLM_BRIEFING` | (unset) | `1` = render the briefing via LLM instead of the deterministic template (opt-in; adds latency on the critical path) |
|
|
90
|
+
| `DAIMON_LLM_BASE_URL` | `LITELLM_BASE_URL` → `http://localhost:4000` | OpenAI-compatible gateway base URL |
|
|
91
|
+
| `DAIMON_LLM_API_KEY` | `LITELLM_API_KEY` | Gateway API key (required to call the LLM) |
|
|
92
|
+
| `DAIMON_LLM_MODEL` | `LITELLM_MODEL` | Model name to use |
|
|
93
|
+
| `DAIMON_LLM_TEMPERATURE` | `0.0` | Sampling temperature sent with every chat call. Default 0.0 for deterministic extraction; some upstreams (e.g. kimi-k2.6) reject anything but their pinned value — set this to match |
|
|
94
|
+
| `DAIMON_LLM_BACKEND` | `auto` | `auto` (default) = litellm if credentials set, else a CLI; or force `litellm` | `command` | `claude-cli` |
|
|
95
|
+
| `DAIMON_LLM_COMMAND` | (unset) | CLI invocation for `command` backend; prompt piped via stdin |
|
|
96
|
+
| `DAIMON_LLM_COMMAND_OUTPUT` | `text` | `text` or `json:<key>` — how to read stdout |
|
|
97
|
+
| `DAIMON_LLM_FALLBACK` | `1` | auto-fall-back to a command backend when litellm fails |
|
|
98
|
+
|
|
99
|
+
### Pluggable LLM backend
|
|
100
|
+
|
|
101
|
+
By default (`auto`) the serializer uses LiteLLM when credentials are set, otherwise a headless LLM CLI if one is available (e.g. `claude` on PATH). When litellm fails (gateway down, no key),
|
|
102
|
+
daimon auto-falls-back to a command backend if one resolves — zero config
|
|
103
|
+
if Claude Code is installed. Override with any CLI:
|
|
104
|
+
|
|
105
|
+
# codex
|
|
106
|
+
DAIMON_LLM_BACKEND=command
|
|
107
|
+
DAIMON_LLM_COMMAND=codex exec --json
|
|
108
|
+
DAIMON_LLM_COMMAND_OUTPUT=json:... # set to the field holding the text
|
|
109
|
+
|
|
110
|
+
# ollama (raw text out)
|
|
111
|
+
DAIMON_LLM_BACKEND=command
|
|
112
|
+
DAIMON_LLM_COMMAND=ollama run llama3
|
|
113
|
+
DAIMON_LLM_COMMAND_OUTPUT=text
|
|
114
|
+
|
|
115
|
+
The prompt is piped via stdin; the CLI runs isolated (`DAIMON_DISABLE=1`, temp
|
|
116
|
+
cwd) so an agent CLI cannot recurse into daimon's own hooks.
|
|
117
|
+
|
|
118
|
+
## Dogfood without hermes
|
|
119
|
+
|
|
120
|
+
The CLI works on any plain-text/markdown transcript, no hermes needed. It uses the
|
|
121
|
+
same env-driven LLM client. The command is `daimon`; `daimon-briefing` remains a
|
|
122
|
+
deprecated alias for one release and will be removed afterward.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
export LITELLM_API_KEY=sk-... # or DAIMON_LLM_API_KEY
|
|
126
|
+
export LITELLM_MODEL=<model-name> # or DAIMON_LLM_MODEL
|
|
127
|
+
export LITELLM_BASE_URL=http://localhost:4000 # or DAIMON_LLM_BASE_URL
|
|
128
|
+
|
|
129
|
+
# 1. Serialize a transcript file into a checkpoint:
|
|
130
|
+
daimon serialize path/to/transcript.md
|
|
131
|
+
# → writes ~/.daimon/checkpoints/<transcript-stem>.json and updates latest.json
|
|
132
|
+
# → with DAIMON_PROJECT_DIR set, also updates <project-slug>/latest.json
|
|
133
|
+
|
|
134
|
+
# 2. Render the "while you were away" briefing from the latest checkpoint:
|
|
135
|
+
daimon brief
|
|
136
|
+
# → prints the briefing to stdout
|
|
137
|
+
# → with DAIMON_PROJECT_DIR set, prefers that project's latest.json
|
|
138
|
+
# (global latest.json is the fallback)
|
|
139
|
+
|
|
140
|
+
# 3. Check whether a checkpoint actually got written (no log grepping):
|
|
141
|
+
daimon status [--project DIR] [--json]
|
|
142
|
+
# → project checkpoint + global fallback: session id, age, path
|
|
143
|
+
# → last serialize outcome from ~/.daimon/logs/serialize.log
|
|
144
|
+
# (success with duration, error, or "no serialize history")
|
|
145
|
+
# → project resolution: --project > DAIMON_PROJECT_DIR > cwd
|
|
146
|
+
# → exit 0 if a project or global checkpoint exists, 1 if neither
|
|
147
|
+
# (scripts can test existence cheaply); --json for machine-readable output
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Transcript format: markdown with `**user**:` / `**assistant**:` role markers (or
|
|
151
|
+
`user:` / `assistant:`), or plain text (treated as a single user message).
|
|
152
|
+
|
|
153
|
+
## What Slice 2 does NOT do
|
|
154
|
+
|
|
155
|
+
- **No regression-gate validation yet** — chunked extraction is implemented but the
|
|
156
|
+
§3 harness gates (RR ≥70%, FMR ≤10%, staleness rate), the S2 probe rerun, the
|
|
157
|
+
holdout, and the 2-cycle test are still owed (Slice 2 part 2, in `research/`).
|
|
158
|
+
- **No Honcho** — checkpoints are local files only. Honcho-backed store + cross-session
|
|
159
|
+
recall is **Slice 3**.
|
|
160
|
+
- **No Claimify gate / Graphiti PR** — **Slice 4**, independent.
|
|
161
|
+
- No proactive interruption, no multi-platform, no checkpoint versioning/rollback.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Daimon Dream-Briefing — hermes plugin (Slice 2)
|
|
2
|
+
|
|
3
|
+
A **dream-briefing** is a session-*start* artifact: a skimmable "while you were
|
|
4
|
+
away / here's where we left off" briefing the agent shows you when you resume work,
|
|
5
|
+
reconstructed from a cognitive checkpoint written at the end of the prior session.
|
|
6
|
+
|
|
7
|
+
This is **Slice 2**: local-file checkpoints, no Honcho. Serialization is
|
|
8
|
+
single-pass for short sessions and chunked multi-pass (armC: per-chunk D-007
|
|
9
|
+
serialize → 01c merge with Q-STALE latest-state preference) above
|
|
10
|
+
`DAIMON_CHUNK_LINES` rendered lines. Failures are named (`SerializeError`
|
|
11
|
+
subclasses) so the CLI and logs say what actually broke.
|
|
12
|
+
Dogfoodable in hermes immediately, and runnable standalone on a plain transcript
|
|
13
|
+
file via the CLI (no hermes required).
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
SESSION N ── on_session_end ──► read transcript (SessionDB)
|
|
19
|
+
└► serialize (D-007 prompt + LLM) → validate → ~/.daimon/checkpoints/<id>.json
|
|
20
|
+
|
|
21
|
+
SESSION N+1 ── first pre_llm_call ──► load latest checkpoint
|
|
22
|
+
└► render briefing (deterministic template)
|
|
23
|
+
└► return {"context": briefing} → appended to your first user message
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The briefing puts **open loops first**, flags items whose state may have changed
|
|
27
|
+
**outside the AI session** (the PR-merge gap) with a *verify before trusting* marker,
|
|
28
|
+
then lists decisions and beliefs. Verbatim (extractively-pinned, D-006) facts are
|
|
29
|
+
marked distinctly from inferred ones.
|
|
30
|
+
|
|
31
|
+
## Install (in hermes)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# From a published repo / package:
|
|
35
|
+
hermes plugins install owner/daimon-plugin --enable
|
|
36
|
+
|
|
37
|
+
# Local editable (development):
|
|
38
|
+
uv pip install -e . # registers the hermes_agent.plugins entry point
|
|
39
|
+
# or copy this directory into ~/.hermes/plugins/daimon-briefing/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The plugin registers two hooks (`on_session_end`, `pre_llm_call`) and bundles the
|
|
43
|
+
user-facing skill as `daimon-briefing:daimon-briefing` (loadable via
|
|
44
|
+
`skill_view("daimon-briefing:daimon-briefing")`).
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
All config is via environment variables. `DAIMON_*` takes precedence; LLM settings
|
|
49
|
+
fall back to the Track-A `LITELLM_*` vars.
|
|
50
|
+
|
|
51
|
+
Every variable also resolves from `~/.daimon/env` when absent from the process
|
|
52
|
+
environment (process env always wins; override the file location with
|
|
53
|
+
`DAIMON_ENV_FILE`). This is how hooks get credentials: a hook inherits whatever
|
|
54
|
+
environment the host process was launched with — a GUI-launched Claude Code has
|
|
55
|
+
no shell profile — so shell exports are not a reliable channel. Format is plain
|
|
56
|
+
`KEY=VALUE` lines (`export ` prefix, quotes, and `#` comments tolerated):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# ~/.daimon/env — chmod 600, it holds API keys
|
|
60
|
+
DAIMON_LLM_API_KEY=sk-...
|
|
61
|
+
DAIMON_LLM_MODEL=<model-name>
|
|
62
|
+
DAIMON_LLM_BASE_URL=http://localhost:4000
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Variable | Default | Purpose |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `DAIMON_DISABLE` | (unset) | `1` = kill switch; hooks become no-ops |
|
|
68
|
+
| `DAIMON_CHECKPOINT_DIR` | `~/.daimon/checkpoints` | Where checkpoints + `latest.json` live |
|
|
69
|
+
| `DAIMON_LOG_DIR` | `~/.daimon/logs` | Where `status` looks for `serialize.log` (the session-end hook writes there) |
|
|
70
|
+
| `DAIMON_PROJECT_DIR` | (unset) | Working directory of the session, for per-project routing. When set, `serialize` also writes `<checkpoint-dir>/<project-slug>/latest.json` and `brief` prefers it (falling back to the global `latest.json`). The Claude Code hooks set this from the payload `cwd`; unset = project unknown = global-only behavior |
|
|
71
|
+
| `DAIMON_MIN_MESSAGES` | `10` | Skip serialization for sessions shorter than this |
|
|
72
|
+
| `DAIMON_TIMEOUT` | `120` | TOTAL budget (seconds) for the session-end serialize LLM work. A deadline is computed at hook start and shared across all retry attempts: per-attempt socket timeouts are capped to the remaining budget, and retries stop when it is exhausted |
|
|
73
|
+
| `DAIMON_CHUNK_LINES` | `1200` | Rendered-transcript line count above which serialization goes chunked (armC: per-chunk serialize → merge). 1200 matches the D-007 recall cliff |
|
|
74
|
+
| `DAIMON_CHUNK_OVERLAP` | `100` | Lines shared between consecutive chunks so boundary decisions aren't lost |
|
|
75
|
+
| `DAIMON_CHUNK_CONCURRENCY` | `4` | Parallel chunk-serialize calls. Gateway calls are generation-bound (~minutes each); sequential chunking made long sessions take chunk-count × minutes |
|
|
76
|
+
| `DAIMON_LLM_BRIEFING` | (unset) | `1` = render the briefing via LLM instead of the deterministic template (opt-in; adds latency on the critical path) |
|
|
77
|
+
| `DAIMON_LLM_BASE_URL` | `LITELLM_BASE_URL` → `http://localhost:4000` | OpenAI-compatible gateway base URL |
|
|
78
|
+
| `DAIMON_LLM_API_KEY` | `LITELLM_API_KEY` | Gateway API key (required to call the LLM) |
|
|
79
|
+
| `DAIMON_LLM_MODEL` | `LITELLM_MODEL` | Model name to use |
|
|
80
|
+
| `DAIMON_LLM_TEMPERATURE` | `0.0` | Sampling temperature sent with every chat call. Default 0.0 for deterministic extraction; some upstreams (e.g. kimi-k2.6) reject anything but their pinned value — set this to match |
|
|
81
|
+
| `DAIMON_LLM_BACKEND` | `auto` | `auto` (default) = litellm if credentials set, else a CLI; or force `litellm` | `command` | `claude-cli` |
|
|
82
|
+
| `DAIMON_LLM_COMMAND` | (unset) | CLI invocation for `command` backend; prompt piped via stdin |
|
|
83
|
+
| `DAIMON_LLM_COMMAND_OUTPUT` | `text` | `text` or `json:<key>` — how to read stdout |
|
|
84
|
+
| `DAIMON_LLM_FALLBACK` | `1` | auto-fall-back to a command backend when litellm fails |
|
|
85
|
+
|
|
86
|
+
### Pluggable LLM backend
|
|
87
|
+
|
|
88
|
+
By default (`auto`) the serializer uses LiteLLM when credentials are set, otherwise a headless LLM CLI if one is available (e.g. `claude` on PATH). When litellm fails (gateway down, no key),
|
|
89
|
+
daimon auto-falls-back to a command backend if one resolves — zero config
|
|
90
|
+
if Claude Code is installed. Override with any CLI:
|
|
91
|
+
|
|
92
|
+
# codex
|
|
93
|
+
DAIMON_LLM_BACKEND=command
|
|
94
|
+
DAIMON_LLM_COMMAND=codex exec --json
|
|
95
|
+
DAIMON_LLM_COMMAND_OUTPUT=json:... # set to the field holding the text
|
|
96
|
+
|
|
97
|
+
# ollama (raw text out)
|
|
98
|
+
DAIMON_LLM_BACKEND=command
|
|
99
|
+
DAIMON_LLM_COMMAND=ollama run llama3
|
|
100
|
+
DAIMON_LLM_COMMAND_OUTPUT=text
|
|
101
|
+
|
|
102
|
+
The prompt is piped via stdin; the CLI runs isolated (`DAIMON_DISABLE=1`, temp
|
|
103
|
+
cwd) so an agent CLI cannot recurse into daimon's own hooks.
|
|
104
|
+
|
|
105
|
+
## Dogfood without hermes
|
|
106
|
+
|
|
107
|
+
The CLI works on any plain-text/markdown transcript, no hermes needed. It uses the
|
|
108
|
+
same env-driven LLM client. The command is `daimon`; `daimon-briefing` remains a
|
|
109
|
+
deprecated alias for one release and will be removed afterward.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export LITELLM_API_KEY=sk-... # or DAIMON_LLM_API_KEY
|
|
113
|
+
export LITELLM_MODEL=<model-name> # or DAIMON_LLM_MODEL
|
|
114
|
+
export LITELLM_BASE_URL=http://localhost:4000 # or DAIMON_LLM_BASE_URL
|
|
115
|
+
|
|
116
|
+
# 1. Serialize a transcript file into a checkpoint:
|
|
117
|
+
daimon serialize path/to/transcript.md
|
|
118
|
+
# → writes ~/.daimon/checkpoints/<transcript-stem>.json and updates latest.json
|
|
119
|
+
# → with DAIMON_PROJECT_DIR set, also updates <project-slug>/latest.json
|
|
120
|
+
|
|
121
|
+
# 2. Render the "while you were away" briefing from the latest checkpoint:
|
|
122
|
+
daimon brief
|
|
123
|
+
# → prints the briefing to stdout
|
|
124
|
+
# → with DAIMON_PROJECT_DIR set, prefers that project's latest.json
|
|
125
|
+
# (global latest.json is the fallback)
|
|
126
|
+
|
|
127
|
+
# 3. Check whether a checkpoint actually got written (no log grepping):
|
|
128
|
+
daimon status [--project DIR] [--json]
|
|
129
|
+
# → project checkpoint + global fallback: session id, age, path
|
|
130
|
+
# → last serialize outcome from ~/.daimon/logs/serialize.log
|
|
131
|
+
# (success with duration, error, or "no serialize history")
|
|
132
|
+
# → project resolution: --project > DAIMON_PROJECT_DIR > cwd
|
|
133
|
+
# → exit 0 if a project or global checkpoint exists, 1 if neither
|
|
134
|
+
# (scripts can test existence cheaply); --json for machine-readable output
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Transcript format: markdown with `**user**:` / `**assistant**:` role markers (or
|
|
138
|
+
`user:` / `assistant:`), or plain text (treated as a single user message).
|
|
139
|
+
|
|
140
|
+
## What Slice 2 does NOT do
|
|
141
|
+
|
|
142
|
+
- **No regression-gate validation yet** — chunked extraction is implemented but the
|
|
143
|
+
§3 harness gates (RR ≥70%, FMR ≤10%, staleness rate), the S2 probe rerun, the
|
|
144
|
+
holdout, and the 2-cycle test are still owed (Slice 2 part 2, in `research/`).
|
|
145
|
+
- **No Honcho** — checkpoints are local files only. Honcho-backed store + cross-session
|
|
146
|
+
recall is **Slice 3**.
|
|
147
|
+
- **No Claimify gate / Graphiti PR** — **Slice 4**, independent.
|
|
148
|
+
- No proactive interruption, no multi-platform, no checkpoint versioning/rollback.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Daimon dream-briefing — hermes plugin entrypoint (Slice 1, local-file, no Honcho)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from . import hooks
|
|
6
|
+
|
|
7
|
+
__version__ = "0.2.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(ctx):
|
|
11
|
+
"""Called once at hermes startup. Wires the two hooks and bundles the skill.
|
|
12
|
+
|
|
13
|
+
# VERIFIED website/docs/guides/build-a-hermes-plugin.md:
|
|
14
|
+
# ctx.register_hook("<event>", callback)
|
|
15
|
+
# ctx.register_skill(skill_name: str, skill_md_path: Path)
|
|
16
|
+
"""
|
|
17
|
+
ctx.register_hook("on_session_end", hooks.on_session_end)
|
|
18
|
+
ctx.register_hook("pre_llm_call", hooks.pre_llm_call)
|
|
19
|
+
|
|
20
|
+
skills_dir = Path(__file__).parent.parent / "skills"
|
|
21
|
+
if skills_dir.is_dir():
|
|
22
|
+
for child in sorted(skills_dir.iterdir()):
|
|
23
|
+
skill_md = child / "SKILL.md"
|
|
24
|
+
if child.is_dir() and skill_md.exists():
|
|
25
|
+
ctx.register_skill(child.name, skill_md)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Anchor cognitive items to code symbols and detect drift — stdlib only.
|
|
2
|
+
|
|
3
|
+
A symbol is identified by (file, symbol) where symbol is `name` or `Class.method`.
|
|
4
|
+
The fingerprint is a structural hash (`ast.dump` of the def node), so it is stable
|
|
5
|
+
to formatting/comments/line-shift and changes only on real structural edits. No MCP,
|
|
6
|
+
no LLM, no network — resolution and drift checks read the project's own source.
|
|
7
|
+
|
|
8
|
+
Caveat: `ast.dump` output is stable only WITHIN a Python version. A checkpoint anchored
|
|
9
|
+
under one interpreter and checked under another may report a spurious "soft" drift — it
|
|
10
|
+
fails safe (toward verify-before-trusting, never a false "live"), and anchors are normally
|
|
11
|
+
resolved and checked by the same interpreter.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import hashlib
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
_DEF = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _find_node(tree: ast.AST, symbol: str):
|
|
22
|
+
nodes = getattr(tree, "body", [])
|
|
23
|
+
node = None
|
|
24
|
+
for part in symbol.split("."):
|
|
25
|
+
node = next(
|
|
26
|
+
(n for n in nodes if isinstance(n, _DEF) and n.name == part), None
|
|
27
|
+
)
|
|
28
|
+
if node is None:
|
|
29
|
+
return None
|
|
30
|
+
nodes = getattr(node, "body", [])
|
|
31
|
+
return node
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def body_hash_of(source: str, symbol: str) -> str | None:
|
|
35
|
+
try:
|
|
36
|
+
tree = ast.parse(source)
|
|
37
|
+
except SyntaxError:
|
|
38
|
+
return None
|
|
39
|
+
node = _find_node(tree, symbol)
|
|
40
|
+
if node is None:
|
|
41
|
+
return None
|
|
42
|
+
return hashlib.sha256(ast.dump(node).encode("utf-8")).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve(project_root, file: str, symbol: str) -> dict | None:
|
|
46
|
+
"""Snapshot an anchor for (file, symbol), or None if it can't be resolved."""
|
|
47
|
+
try:
|
|
48
|
+
source = (Path(project_root) / file).read_text(encoding="utf-8")
|
|
49
|
+
except OSError:
|
|
50
|
+
return None
|
|
51
|
+
h = body_hash_of(source, symbol)
|
|
52
|
+
if h is None:
|
|
53
|
+
return None
|
|
54
|
+
return {
|
|
55
|
+
"qualified_name": f"{file}::{symbol}",
|
|
56
|
+
"file": file,
|
|
57
|
+
"symbol": symbol,
|
|
58
|
+
"body_hash": h,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check(anchor: dict, project_root) -> str:
|
|
63
|
+
"""Classify drift: 'live' (unchanged), 'soft' (body changed), 'hard' (gone/unverifiable).
|
|
64
|
+
|
|
65
|
+
Degrades on a malformed anchor (missing/non-str file or symbol) by returning
|
|
66
|
+
'hard' — the offline check must never raise on hand-edited checkpoint data."""
|
|
67
|
+
file = anchor.get("file")
|
|
68
|
+
symbol = anchor.get("symbol")
|
|
69
|
+
if not isinstance(file, str) or not isinstance(symbol, str):
|
|
70
|
+
return "hard"
|
|
71
|
+
try:
|
|
72
|
+
source = (Path(project_root) / file).read_text(encoding="utf-8")
|
|
73
|
+
except OSError:
|
|
74
|
+
return "hard"
|
|
75
|
+
h = body_hash_of(source, symbol)
|
|
76
|
+
if h is None:
|
|
77
|
+
return "hard"
|
|
78
|
+
return "live" if h == anchor.get("body_hash") else "soft"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _all_items(checkpoint: dict):
|
|
82
|
+
wc = checkpoint.get("working_context") or {}
|
|
83
|
+
es = checkpoint.get("epistemic_snapshot") or {}
|
|
84
|
+
for key in ("open_questions", "recent_decisions"):
|
|
85
|
+
yield from (wc.get(key) or [])
|
|
86
|
+
for key in ("strong_beliefs", "uncertainties"):
|
|
87
|
+
yield from (es.get(key) or [])
|
|
88
|
+
active = wc.get("active_topic")
|
|
89
|
+
if isinstance(active, dict):
|
|
90
|
+
yield active
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def drifted(checkpoint: dict, project_root) -> list[dict]:
|
|
94
|
+
"""Anchored items whose code has drifted (soft/hard); live ones omitted."""
|
|
95
|
+
out = []
|
|
96
|
+
for item in _all_items(checkpoint):
|
|
97
|
+
a = item.get("anchored_to") if isinstance(item, dict) else None
|
|
98
|
+
if not isinstance(a, dict):
|
|
99
|
+
continue
|
|
100
|
+
kind = check(a, project_root)
|
|
101
|
+
if kind != "live":
|
|
102
|
+
out.append({"item": item, "kind": kind, "anchor": a})
|
|
103
|
+
return out
|