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.
Files changed (47) hide show
  1. daimon_briefing-0.3.0/.gitignore +4 -0
  2. daimon_briefing-0.3.0/PKG-INFO +161 -0
  3. daimon_briefing-0.3.0/README.md +148 -0
  4. daimon_briefing-0.3.0/daimon_briefing/__init__.py +25 -0
  5. daimon_briefing-0.3.0/daimon_briefing/anchor.py +103 -0
  6. daimon_briefing-0.3.0/daimon_briefing/briefing.py +274 -0
  7. daimon_briefing-0.3.0/daimon_briefing/carry.py +97 -0
  8. daimon_briefing-0.3.0/daimon_briefing/cli.py +1119 -0
  9. daimon_briefing-0.3.0/daimon_briefing/config.py +406 -0
  10. daimon_briefing-0.3.0/daimon_briefing/configure.py +81 -0
  11. daimon_briefing-0.3.0/daimon_briefing/harvest.py +189 -0
  12. daimon_briefing-0.3.0/daimon_briefing/hooks.py +78 -0
  13. daimon_briefing-0.3.0/daimon_briefing/llm.py +239 -0
  14. daimon_briefing-0.3.0/daimon_briefing/recall.py +588 -0
  15. daimon_briefing-0.3.0/daimon_briefing/render.py +389 -0
  16. daimon_briefing-0.3.0/daimon_briefing/scoring.py +79 -0
  17. daimon_briefing-0.3.0/daimon_briefing/serializer.py +550 -0
  18. daimon_briefing-0.3.0/daimon_briefing/store.py +506 -0
  19. daimon_briefing-0.3.0/daimon_briefing/teamsync.py +484 -0
  20. daimon_briefing-0.3.0/daimon_briefing/transcript.py +258 -0
  21. daimon_briefing-0.3.0/pyproject.toml +33 -0
  22. daimon_briefing-0.3.0/skills/daimon-briefing/SKILL.md +49 -0
  23. daimon_briefing-0.3.0/skills/daimon-end/SKILL.md +74 -0
  24. daimon_briefing-0.3.0/tests/__init__.py +0 -0
  25. daimon_briefing-0.3.0/tests/conftest.py +119 -0
  26. daimon_briefing-0.3.0/tests/fixtures/sample_transcript.md +18 -0
  27. daimon_briefing-0.3.0/tests/test_anchor.py +137 -0
  28. daimon_briefing-0.3.0/tests/test_briefing.py +448 -0
  29. daimon_briefing-0.3.0/tests/test_carry.py +147 -0
  30. daimon_briefing-0.3.0/tests/test_claude_hooks.py +506 -0
  31. daimon_briefing-0.3.0/tests/test_cli.py +2321 -0
  32. daimon_briefing-0.3.0/tests/test_codex_hooks.py +330 -0
  33. daimon_briefing-0.3.0/tests/test_config.py +403 -0
  34. daimon_briefing-0.3.0/tests/test_configure.py +153 -0
  35. daimon_briefing-0.3.0/tests/test_gemini_hooks.py +372 -0
  36. daimon_briefing-0.3.0/tests/test_harvest.py +241 -0
  37. daimon_briefing-0.3.0/tests/test_hooks.py +344 -0
  38. daimon_briefing-0.3.0/tests/test_isolation.py +36 -0
  39. daimon_briefing-0.3.0/tests/test_llm.py +361 -0
  40. daimon_briefing-0.3.0/tests/test_recall.py +625 -0
  41. daimon_briefing-0.3.0/tests/test_render.py +486 -0
  42. daimon_briefing-0.3.0/tests/test_scoring.py +79 -0
  43. daimon_briefing-0.3.0/tests/test_serializer.py +866 -0
  44. daimon_briefing-0.3.0/tests/test_store.py +751 -0
  45. daimon_briefing-0.3.0/tests/test_teamsync.py +473 -0
  46. daimon_briefing-0.3.0/tests/test_transcript.py +215 -0
  47. daimon_briefing-0.3.0/uv.lock +195 -0
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ *.egg-info/
@@ -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