eling 0.2.1__tar.gz → 0.2.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.
- {eling-0.2.1/src/eling.egg-info → eling-0.2.2}/PKG-INFO +50 -3
- {eling-0.2.1 → eling-0.2.2}/README.md +48 -1
- {eling-0.2.1 → eling-0.2.2}/pyproject.toml +2 -2
- {eling-0.2.1 → eling-0.2.2}/src/eling/__init__.py +9 -4
- {eling-0.2.1 → eling-0.2.2}/src/eling/brain.py +39 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/config.py +8 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/hooks.py +55 -8
- {eling-0.2.1 → eling-0.2.2}/src/eling/mcp_server.py +37 -0
- eling-0.2.2/src/eling/verify_on_stop.py +307 -0
- {eling-0.2.1 → eling-0.2.2/src/eling.egg-info}/PKG-INFO +50 -3
- {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/SOURCES.txt +3 -1
- {eling-0.2.1 → eling-0.2.2}/tests/test_export.py +1 -1
- {eling-0.2.1 → eling-0.2.2}/tests/test_hooks.py +8 -7
- {eling-0.2.1 → eling-0.2.2}/tests/test_think.py +1 -1
- eling-0.2.2/tests/test_verify_on_stop.py +171 -0
- {eling-0.2.1 → eling-0.2.2}/LICENSE +0 -0
- {eling-0.2.1 → eling-0.2.2}/setup.cfg +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/adapters/__init__.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/cli.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/compress.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/decay.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/export.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/hermes_plugin.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/__init__.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/builtin.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/code.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/code_index.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/facts.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/hrr.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/kb.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/layers/notion.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/permissions.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/privacy.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/scripts/benchmark.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/snapshot.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling/utils/__init__.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/dependency_links.txt +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/requires.txt +0 -0
- {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/top_level.txt +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_adapters.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_brain.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_builtin.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_cli.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_compress.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_config.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_contradiction.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_decay.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_facts.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_graph.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_hrr.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_kb.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_permissions.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_privacy.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_schema_packs.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_snapshot.py +0 -0
- {eling-0.2.1 → eling-0.2.2}/tests/test_sync.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eling
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: Unified second brain for AI agents — 5-tier memory, HRR reasoning,
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Unified second brain for AI agents — 5-tier memory, HRR reasoning, 10 MCP tools, conditional verify-on-stop
|
|
5
5
|
Author: PatrickNoFilter
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: memory,mcp,ai-agent,second-brain,hrr
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
# 🧠 Eling
|
|
30
30
|
|
|
31
|
-
**Unified second brain for AI agents — 5-tier memory, HRR reasoning,
|
|
31
|
+
**Unified second brain for AI agents — 5-tier memory, HRR reasoning, 10 MCP tools, conditional verify-on-stop**
|
|
32
32
|
|
|
33
33
|
*"Eling" (Javanese): to remember, to be conscious, to be aware*
|
|
34
34
|
|
|
@@ -66,6 +66,7 @@ All accessible via **9 MCP tools** from a single stdio server:
|
|
|
66
66
|
| `eling_stats` | Show per-layer statistics |
|
|
67
67
|
| `eling_think` | Synthesis + gap analysis across layers |
|
|
68
68
|
| `eling_export` | Full brain export as JSON or Markdown |
|
|
69
|
+
| `eling_verify` | Query/record verification status (conditional) |
|
|
69
70
|
|
|
70
71
|
## 🚀 Quick Start
|
|
71
72
|
|
|
@@ -200,6 +201,52 @@ print(result) # {"layer": "notion", "page_id": "...", ...}
|
|
|
200
201
|
|
|
201
202
|
> **Note**: `eling_reflect` and `remember(layer="notion")` check availability at call time and return a clear error if any config is missing — no silent failures.
|
|
202
203
|
|
|
204
|
+
## 🛡️ Verify-on-Stop (Conditional)
|
|
205
|
+
|
|
206
|
+
Eling provides **verify-on-stop** nudges for AI agents that lack built-in
|
|
207
|
+
verification (e.g., OpenCode, OpenClaw, Cursor, Windsurf). When running under
|
|
208
|
+
Hermes, this feature automatically **skips** — because Hermes already has its
|
|
209
|
+
own `agent/verification_stop.py`.
|
|
210
|
+
|
|
211
|
+
### How it works
|
|
212
|
+
|
|
213
|
+
1. **Auto-detection** — Eling detects the host agent from environment variables
|
|
214
|
+
(`HERMES_SESSION_SOURCE` → Hermes, `OPENCODE_HOME` → OpenCode, etc.)
|
|
215
|
+
2. **File edit tracking** — When code files are edited via hooks or MCP tools,
|
|
216
|
+
eling records them in a verification ledger
|
|
217
|
+
3. **Verification nudge** — If code was edited but no passing tests/verification
|
|
218
|
+
was recorded, eling produces a `[System: ...]` nudge message
|
|
219
|
+
4. **Recording** — Agents can call `eling_verify` MCP tool to record verification
|
|
220
|
+
results (`passed`, `failed`, `skipped`)
|
|
221
|
+
|
|
222
|
+
### Usage via MCP
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
// Query current status
|
|
226
|
+
{ "method": "tools/call", "params": { "name": "eling_verify", "arguments": {} } }
|
|
227
|
+
|
|
228
|
+
// Record a passing verification
|
|
229
|
+
{ "method": "tools/call", "params": {
|
|
230
|
+
"name": "eling_verify",
|
|
231
|
+
"arguments": { "status": "passed", "command": "pytest", "output": "364 passed" }
|
|
232
|
+
} }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Config
|
|
236
|
+
|
|
237
|
+
| Key | Default | Env | Description |
|
|
238
|
+
|-----|---------|-----|-------------|
|
|
239
|
+
| `verify_on_stop` | `true` | `ELING_VERIFY_ON_STOP` | Enable nudges for non-Hermes agents |
|
|
240
|
+
| `verify_on_stop_max_attempts` | `2` | `ELING_VERIFY_MAX_ATTEMPTS` | Max nudges per session |
|
|
241
|
+
| `adapter` | `hermes` | `ELING_ADAPTER` | Force adapter type |
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
plugins:
|
|
245
|
+
eling:
|
|
246
|
+
adapter: auto # auto-detect from env
|
|
247
|
+
verify_on_stop: true
|
|
248
|
+
```
|
|
249
|
+
|
|
203
250
|
## 🏗️ Architecture
|
|
204
251
|
|
|
205
252
|
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# 🧠 Eling
|
|
4
4
|
|
|
5
|
-
**Unified second brain for AI agents — 5-tier memory, HRR reasoning,
|
|
5
|
+
**Unified second brain for AI agents — 5-tier memory, HRR reasoning, 10 MCP tools, conditional verify-on-stop**
|
|
6
6
|
|
|
7
7
|
*"Eling" (Javanese): to remember, to be conscious, to be aware*
|
|
8
8
|
|
|
@@ -40,6 +40,7 @@ All accessible via **9 MCP tools** from a single stdio server:
|
|
|
40
40
|
| `eling_stats` | Show per-layer statistics |
|
|
41
41
|
| `eling_think` | Synthesis + gap analysis across layers |
|
|
42
42
|
| `eling_export` | Full brain export as JSON or Markdown |
|
|
43
|
+
| `eling_verify` | Query/record verification status (conditional) |
|
|
43
44
|
|
|
44
45
|
## 🚀 Quick Start
|
|
45
46
|
|
|
@@ -174,6 +175,52 @@ print(result) # {"layer": "notion", "page_id": "...", ...}
|
|
|
174
175
|
|
|
175
176
|
> **Note**: `eling_reflect` and `remember(layer="notion")` check availability at call time and return a clear error if any config is missing — no silent failures.
|
|
176
177
|
|
|
178
|
+
## 🛡️ Verify-on-Stop (Conditional)
|
|
179
|
+
|
|
180
|
+
Eling provides **verify-on-stop** nudges for AI agents that lack built-in
|
|
181
|
+
verification (e.g., OpenCode, OpenClaw, Cursor, Windsurf). When running under
|
|
182
|
+
Hermes, this feature automatically **skips** — because Hermes already has its
|
|
183
|
+
own `agent/verification_stop.py`.
|
|
184
|
+
|
|
185
|
+
### How it works
|
|
186
|
+
|
|
187
|
+
1. **Auto-detection** — Eling detects the host agent from environment variables
|
|
188
|
+
(`HERMES_SESSION_SOURCE` → Hermes, `OPENCODE_HOME` → OpenCode, etc.)
|
|
189
|
+
2. **File edit tracking** — When code files are edited via hooks or MCP tools,
|
|
190
|
+
eling records them in a verification ledger
|
|
191
|
+
3. **Verification nudge** — If code was edited but no passing tests/verification
|
|
192
|
+
was recorded, eling produces a `[System: ...]` nudge message
|
|
193
|
+
4. **Recording** — Agents can call `eling_verify` MCP tool to record verification
|
|
194
|
+
results (`passed`, `failed`, `skipped`)
|
|
195
|
+
|
|
196
|
+
### Usage via MCP
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
// Query current status
|
|
200
|
+
{ "method": "tools/call", "params": { "name": "eling_verify", "arguments": {} } }
|
|
201
|
+
|
|
202
|
+
// Record a passing verification
|
|
203
|
+
{ "method": "tools/call", "params": {
|
|
204
|
+
"name": "eling_verify",
|
|
205
|
+
"arguments": { "status": "passed", "command": "pytest", "output": "364 passed" }
|
|
206
|
+
} }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Config
|
|
210
|
+
|
|
211
|
+
| Key | Default | Env | Description |
|
|
212
|
+
|-----|---------|-----|-------------|
|
|
213
|
+
| `verify_on_stop` | `true` | `ELING_VERIFY_ON_STOP` | Enable nudges for non-Hermes agents |
|
|
214
|
+
| `verify_on_stop_max_attempts` | `2` | `ELING_VERIFY_MAX_ATTEMPTS` | Max nudges per session |
|
|
215
|
+
| `adapter` | `hermes` | `ELING_ADAPTER` | Force adapter type |
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
plugins:
|
|
219
|
+
eling:
|
|
220
|
+
adapter: auto # auto-detect from env
|
|
221
|
+
verify_on_stop: true
|
|
222
|
+
```
|
|
223
|
+
|
|
177
224
|
## 🏗️ Architecture
|
|
178
225
|
|
|
179
226
|
```
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "eling"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "Unified second brain for AI agents — 5-tier memory, HRR reasoning,
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Unified second brain for AI agents — 5-tier memory, HRR reasoning, 10 MCP tools, conditional verify-on-stop"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""Eling — unified second brain for AI agents.
|
|
2
2
|
|
|
3
3
|
5-layer architecture: builtin / facts / kb / code / notion
|
|
4
|
+
Features: HRR reasoning, gap analysis, Notion auto-sync, verify-on-stop.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
|
-
__version__ = "0.2.
|
|
7
|
-
__all__ = [
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
__version__ = "0.2.2"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Brain", "HookRegistry", "ALL_HOOKS", "register_default_hooks",
|
|
10
|
+
"remember", "recall", "reason", "resolve_config", "set_config_key",
|
|
11
|
+
"get_config", "describe_config",
|
|
12
|
+
"verify_on_stop", "detect_host_agent", "host_has_verify_on_stop",
|
|
13
|
+
]
|
|
10
14
|
|
|
11
15
|
from .brain import Brain
|
|
12
16
|
from .hooks import HookRegistry, ALL_HOOKS, register_default_hooks
|
|
13
17
|
from .config import resolve_config, set_config_key, get_config, describe_config
|
|
18
|
+
from . import verify_on_stop
|
|
14
19
|
|
|
15
20
|
_default_brain: Brain | None = None
|
|
16
21
|
|
|
@@ -105,6 +105,7 @@ class Brain:
|
|
|
105
105
|
notion_parent_id: str | None = None,
|
|
106
106
|
project_path: str | Path | None = None,
|
|
107
107
|
hrr_dim: int = 1024,
|
|
108
|
+
adapter: str | None = None,
|
|
108
109
|
):
|
|
109
110
|
self.home = Path(home).expanduser() if home else _eling_home()
|
|
110
111
|
self.home.mkdir(parents=True, exist_ok=True)
|
|
@@ -120,6 +121,8 @@ class Brain:
|
|
|
120
121
|
# Hooks registry
|
|
121
122
|
self.hooks = eling_hooks.HookRegistry()
|
|
122
123
|
eling_hooks.register_default_hooks(self)
|
|
124
|
+
# Adapter for verify-on-stop (hermes | opencode | openclaw | auto)
|
|
125
|
+
self._adapter: str = adapter or "auto"
|
|
123
126
|
|
|
124
127
|
@property
|
|
125
128
|
def _task_logs_id(self) -> str | None:
|
|
@@ -495,6 +498,42 @@ class Brain:
|
|
|
495
498
|
},
|
|
496
499
|
}
|
|
497
500
|
|
|
501
|
+
# ── verify — check verification-on-stop status ──
|
|
502
|
+
|
|
503
|
+
def verify(self, status: str = "", command: str = "", output: str = "") -> dict:
|
|
504
|
+
"""Query or record verification-on-stop status.
|
|
505
|
+
|
|
506
|
+
When called with no args, returns the current verification status from
|
|
507
|
+
the ledger (including a nudge message if code edits need verification).
|
|
508
|
+
|
|
509
|
+
When called with ``status`` set to ``"passed"``, ``"failed"``, or
|
|
510
|
+
``"skipped"``, records the verification event in the ledger.
|
|
511
|
+
|
|
512
|
+
This is a **conditional** feature — it only activates when the host
|
|
513
|
+
agent does NOT have built-in verify-on-stop (auto-detected from env
|
|
514
|
+
or ``ELING_ADAPTER`` config). When running under Hermes, this is a
|
|
515
|
+
no-op that returns ``{"host_has_verify": True}``.
|
|
516
|
+
"""
|
|
517
|
+
from . import verify_on_stop as vos
|
|
518
|
+
|
|
519
|
+
if vos.host_has_verify_on_stop(adapter=self._adapter):
|
|
520
|
+
return {"host_has_verify": True, "active": False}
|
|
521
|
+
|
|
522
|
+
if status:
|
|
523
|
+
vos.record_verification(status=status, command=command, output=output)
|
|
524
|
+
return {
|
|
525
|
+
"host_has_verify": False,
|
|
526
|
+
"active": True,
|
|
527
|
+
"recorded": True,
|
|
528
|
+
"status": status,
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
"host_has_verify": False,
|
|
533
|
+
"active": True,
|
|
534
|
+
**vos.verify_status(),
|
|
535
|
+
}
|
|
536
|
+
|
|
498
537
|
# ── export — dump all layers (Task 13.2) ──
|
|
499
538
|
|
|
500
539
|
def export(self, format: str = "json", path: str | None = None) -> dict:
|
|
@@ -28,6 +28,8 @@ DEFAULTS: dict[str, Any] = {
|
|
|
28
28
|
"auto_sync_turns": True,
|
|
29
29
|
"schema_pack": "default",
|
|
30
30
|
"adapter": "hermes",
|
|
31
|
+
"verify_on_stop": True,
|
|
32
|
+
"verify_on_stop_max_attempts": 2,
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
ENV_MAP: dict[str, str] = {
|
|
@@ -41,6 +43,8 @@ ENV_MAP: dict[str, str] = {
|
|
|
41
43
|
"auto_sync_turns": "ELING_AUTO_SYNC_TURNS",
|
|
42
44
|
"schema_pack": "ELING_SCHEMA_PACK",
|
|
43
45
|
"adapter": "ELING_ADAPTER",
|
|
46
|
+
"verify_on_stop": "ELING_VERIFY_ON_STOP",
|
|
47
|
+
"verify_on_stop_max_attempts": "ELING_VERIFY_MAX_ATTEMPTS",
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
TYPE_MAP: dict[str, type] = {
|
|
@@ -53,6 +57,8 @@ TYPE_MAP: dict[str, type] = {
|
|
|
53
57
|
"auto_sync_turns": bool,
|
|
54
58
|
"schema_pack": str,
|
|
55
59
|
"adapter": str,
|
|
60
|
+
"verify_on_stop": bool,
|
|
61
|
+
"verify_on_stop_max_attempts": int,
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
# ── Schema packs ──────────────────────────────────────────────────────────────
|
|
@@ -268,4 +274,6 @@ def describe_config() -> dict[str, dict]:
|
|
|
268
274
|
"auto_sync_turns": {"type": "bool", "default": True, "env": "ELING_AUTO_SYNC_TURNS", "description": "Auto-store user/assistant messages"},
|
|
269
275
|
"schema_pack": {"type": "str", "default": "default", "env": "ELING_SCHEMA_PACK", "description": "Category schema pack: default | coding | research"},
|
|
270
276
|
"adapter": {"type": "str", "default": "hermes", "env": "ELING_ADAPTER", "description": "Harness adapter: hermes | claude_cli | opencode | openclaw | openclaude"},
|
|
277
|
+
"verify_on_stop": {"type": "bool", "default": True, "env": "ELING_VERIFY_ON_STOP", "description": "Enable verify-on-stop nudges for non-Hermes agents"},
|
|
278
|
+
"verify_on_stop_max_attempts": {"type": "int", "default": 2, "env": "ELING_VERIFY_MAX_ATTEMPTS", "description": "Max verification nudge retries per session"},
|
|
271
279
|
}
|
|
@@ -51,6 +51,9 @@ HOOK_COMPACTION = "compaction"
|
|
|
51
51
|
HOOK_SESSION_END = "session_end"
|
|
52
52
|
HOOK_IDLE_30MIN = "idle_30min"
|
|
53
53
|
|
|
54
|
+
# ── Verify-on-stop hook ──
|
|
55
|
+
HOOK_VERIFY_REQUEST = "verify_request"
|
|
56
|
+
|
|
54
57
|
# ── Sync hooks ──
|
|
55
58
|
HOOK_SYNC_START = "sync_start"
|
|
56
59
|
HOOK_SYNC_COMPLETE = "sync_complete"
|
|
@@ -65,6 +68,7 @@ ALL_HOOKS = [
|
|
|
65
68
|
HOOK_POST_ASSISTANT_MESSAGE,
|
|
66
69
|
HOOK_DECISION_MADE,
|
|
67
70
|
HOOK_FILE_EDIT,
|
|
71
|
+
HOOK_VERIFY_REQUEST,
|
|
68
72
|
HOOK_ERROR_OCCURRED,
|
|
69
73
|
HOOK_COMPACTION,
|
|
70
74
|
HOOK_SESSION_END,
|
|
@@ -257,16 +261,35 @@ def _make_decision_made_handler(brain: "Brain") -> HookHandler:
|
|
|
257
261
|
|
|
258
262
|
|
|
259
263
|
def _make_file_edit_handler(brain: "Brain") -> HookHandler:
|
|
260
|
-
"""HOOK: file_edit — re-index file in codegraph
|
|
264
|
+
"""HOOK: file_edit — re-index file in codegraph + track in verification ledger."""
|
|
261
265
|
def handler(name: str, ctx: dict) -> dict:
|
|
262
266
|
file_path = ctx.get("file_path", "")
|
|
263
|
-
if not file_path
|
|
264
|
-
return {"reindexed": False}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
if not file_path:
|
|
268
|
+
return {"reindexed": False, "verify_tracked": False}
|
|
269
|
+
result: dict = {}
|
|
270
|
+
# 1. Re-index in codegraph layer if available
|
|
271
|
+
if brain.code.available:
|
|
272
|
+
try:
|
|
273
|
+
brain.code.reindex(file_path)
|
|
274
|
+
result["reindexed"] = True
|
|
275
|
+
except Exception:
|
|
276
|
+
result["reindexed"] = False
|
|
277
|
+
else:
|
|
278
|
+
result["reindexed"] = False
|
|
279
|
+
# 2. Track in verification ledger
|
|
280
|
+
from . import verify_on_stop as vos
|
|
281
|
+
adapter = getattr(brain, "_adapter", "auto")
|
|
282
|
+
if not vos.host_has_verify_on_stop(adapter=adapter):
|
|
283
|
+
vos.record_edit(file_path)
|
|
284
|
+
result["verify_tracked"] = True
|
|
285
|
+
# Fire verify_request hook
|
|
286
|
+
brain.fire_hook(
|
|
287
|
+
HOOK_VERIFY_REQUEST,
|
|
288
|
+
changed_paths=list(vos._ledger.get("changed_paths", [])),
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
result["verify_tracked"] = False
|
|
292
|
+
return result
|
|
270
293
|
return handler
|
|
271
294
|
|
|
272
295
|
|
|
@@ -356,6 +379,29 @@ def _make_idle_30min_handler(brain: "Brain") -> HookHandler:
|
|
|
356
379
|
return handler
|
|
357
380
|
|
|
358
381
|
|
|
382
|
+
def _make_verify_request_handler(brain: "Brain") -> HookHandler:
|
|
383
|
+
"""HOOK: verify_request — verification nudge for non-Hermes agents."""
|
|
384
|
+
def handler(name: str, ctx: dict) -> dict:
|
|
385
|
+
from . import verify_on_stop as vos
|
|
386
|
+
|
|
387
|
+
# Skip if host agent has its own verify-on-stop
|
|
388
|
+
adapter = getattr(brain, "_adapter", "auto")
|
|
389
|
+
if vos.host_has_verify_on_stop(adapter=adapter):
|
|
390
|
+
return {"nudge": None, "reason": "host has verify-on-stop"}
|
|
391
|
+
|
|
392
|
+
changed = ctx.get("changed_paths", [])
|
|
393
|
+
if not changed:
|
|
394
|
+
return {"nudge": None, "reason": "no changed paths"}
|
|
395
|
+
|
|
396
|
+
nudge = vos.build_verify_nudge()
|
|
397
|
+
return {
|
|
398
|
+
"nudge": nudge,
|
|
399
|
+
"changed_paths_count": len(changed),
|
|
400
|
+
"needs_verification": nudge is not None,
|
|
401
|
+
}
|
|
402
|
+
return handler
|
|
403
|
+
|
|
404
|
+
|
|
359
405
|
def _make_noop_handler(brain: "Brain" = None) -> HookHandler: # type: ignore[assignment]
|
|
360
406
|
"""Factory: no-op handler for hooks with no default logic."""
|
|
361
407
|
def handler(name: str, ctx: dict) -> dict:
|
|
@@ -381,6 +427,7 @@ def register_default_hooks(brain: "Brain") -> HookRegistry:
|
|
|
381
427
|
HOOK_POST_ASSISTANT_MESSAGE: _make_post_assistant_message_handler,
|
|
382
428
|
HOOK_DECISION_MADE: _make_decision_made_handler,
|
|
383
429
|
HOOK_FILE_EDIT: _make_file_edit_handler,
|
|
430
|
+
HOOK_VERIFY_REQUEST: _make_verify_request_handler,
|
|
384
431
|
HOOK_ERROR_OCCURRED: _make_error_occurred_handler,
|
|
385
432
|
HOOK_COMPACTION: _make_compaction_handler,
|
|
386
433
|
HOOK_SESSION_END: _make_session_end_handler,
|
|
@@ -229,6 +229,38 @@ TOOLS = [
|
|
|
229
229
|
"required": [],
|
|
230
230
|
},
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
"name": "eling_verify",
|
|
234
|
+
"description": "Check or record verification-on-stop status. "
|
|
235
|
+
"When host agent (Hermes) already has verify-on-stop, returns "
|
|
236
|
+
"{host_has_verify: true, active: false}. "
|
|
237
|
+
"When host agent lacks verification (OpenCode, etc.), returns "
|
|
238
|
+
"current status or records a verification event. "
|
|
239
|
+
"Call with no args to query status; pass status='passed'/'failed'/'skipped' "
|
|
240
|
+
"with optional command and output to record a verification event.",
|
|
241
|
+
"inputSchema": {
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
"status": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"enum": ["", "passed", "failed", "skipped"],
|
|
247
|
+
"default": "",
|
|
248
|
+
"description": "Verification result. Empty = query mode. Set to 'passed'/'failed'/'skipped' to record.",
|
|
249
|
+
},
|
|
250
|
+
"command": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"default": "",
|
|
253
|
+
"description": "The command that was run (e.g. 'pytest')",
|
|
254
|
+
},
|
|
255
|
+
"output": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"default": "",
|
|
258
|
+
"description": "Command output (truncated to 500 chars)",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
"required": [],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
232
264
|
]
|
|
233
265
|
|
|
234
266
|
|
|
@@ -313,6 +345,11 @@ def _handle_tool_call(rid: int | str | None, params: dict) -> dict:
|
|
|
313
345
|
fmt = args.pop("format", "json")
|
|
314
346
|
path = args.pop("path", None) or None
|
|
315
347
|
return ok(brain.export(format=fmt, path=path))
|
|
348
|
+
elif tool_name == "eling_verify":
|
|
349
|
+
status = args.pop("status", "")
|
|
350
|
+
command = args.pop("command", "")
|
|
351
|
+
output = args.pop("output", "")
|
|
352
|
+
return ok(brain.verify(status=status, command=command, output=output))
|
|
316
353
|
else:
|
|
317
354
|
return _error(rid, -32601, f"unknown tool: {tool_name}")
|
|
318
355
|
except Exception as e:
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Verify-on-stop — verification nudge for agents that lack built-in verification.
|
|
2
|
+
|
|
3
|
+
When an AI agent (OpenCode, OpenClaw, etc.) does not have its own
|
|
4
|
+
verify-on-stop, eling fills the gap:
|
|
5
|
+
|
|
6
|
+
1. Tracks file edits via hooks or explicit MCP calls
|
|
7
|
+
2. Detects whether the host agent already has built-in verification (skip)
|
|
8
|
+
3. Produces a verification nudge message when code was edited but not verified
|
|
9
|
+
4. Exposes status via MCP tool so any agent can query it
|
|
10
|
+
|
|
11
|
+
Detection logic:
|
|
12
|
+
- ELING_ADAPTER=hermes → skip (Hermes has built-in verification)
|
|
13
|
+
- ELING_ADAPTER=opencode|openclaw|openclaude|claude_cli → enable
|
|
14
|
+
- ELING_ADAPTER=auto → auto-detect from environment variables
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
import logging
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Agent signatures
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
# Agents that have built-in verify-on-stop — eling is a no-op for these
|
|
32
|
+
AGENTS_WITH_VERIFY: frozenset[str] = frozenset({"hermes"})
|
|
33
|
+
|
|
34
|
+
# Agents that do NOT have built-in verify-on-stop — eling provides it
|
|
35
|
+
AGENTS_WITHOUT_VERIFY: frozenset[str] = frozenset({
|
|
36
|
+
"opencode",
|
|
37
|
+
"openclaw",
|
|
38
|
+
"openclaude",
|
|
39
|
+
"claude_cli",
|
|
40
|
+
"cursor",
|
|
41
|
+
"windsurf",
|
|
42
|
+
"generic",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
# Env-var → agent name mapping for auto-detection
|
|
46
|
+
AGENT_SIGNATURES: dict[str, str] = {
|
|
47
|
+
"HERMES_SESSION_SOURCE": "hermes",
|
|
48
|
+
"HERMES_PLATFORM": "hermes",
|
|
49
|
+
"OPENCODE_HOME": "opencode",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Public API: detection
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def detect_host_agent() -> str:
|
|
58
|
+
"""Detect which AI agent is running by inspecting environment variables.
|
|
59
|
+
|
|
60
|
+
Returns one of: ``hermes``, ``opencode``, or ``generic``.
|
|
61
|
+
"""
|
|
62
|
+
for env_var, agent in AGENT_SIGNATURES.items():
|
|
63
|
+
val = os.environ.get(env_var)
|
|
64
|
+
if val and str(val).strip():
|
|
65
|
+
return agent
|
|
66
|
+
return "generic"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def host_has_verify_on_stop(adapter: str = "auto") -> bool:
|
|
70
|
+
"""Return True if the host agent already has verify-on-stop built-in.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
adapter:
|
|
75
|
+
The resolved ``ELING_ADAPTER`` value.
|
|
76
|
+
``"auto"`` (default) → auto-detect from environment.
|
|
77
|
+
Any other string is checked against ``AGENTS_WITH_VERIFY``.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
bool
|
|
82
|
+
True when the host agent natively handles verification nudges.
|
|
83
|
+
"""
|
|
84
|
+
if adapter != "auto":
|
|
85
|
+
return adapter in AGENTS_WITH_VERIFY
|
|
86
|
+
agent = detect_host_agent()
|
|
87
|
+
return agent in AGENTS_WITH_VERIFY
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Verification ledger (session-scoped)
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
_ledger: dict[str, Any] = {
|
|
95
|
+
"changed_paths": [],
|
|
96
|
+
"verification_events": [],
|
|
97
|
+
"verified": False,
|
|
98
|
+
"last_edit_time": 0.0,
|
|
99
|
+
"last_verify_time": 0.0,
|
|
100
|
+
"verify_attempts": 0,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def record_edit(file_path: str) -> None:
|
|
105
|
+
"""Record a file edit in the verification ledger.
|
|
106
|
+
|
|
107
|
+
Call this whenever the agent writes or patches a file.
|
|
108
|
+
Resets the ``verified`` flag so a new verification is required.
|
|
109
|
+
"""
|
|
110
|
+
global _ledger
|
|
111
|
+
if file_path not in _ledger["changed_paths"]:
|
|
112
|
+
_ledger["changed_paths"].append(file_path)
|
|
113
|
+
_ledger["last_edit_time"] = time.time()
|
|
114
|
+
_ledger["verified"] = False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def record_verification(
|
|
118
|
+
status: str,
|
|
119
|
+
command: str = "",
|
|
120
|
+
output: str = "",
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Record a verification event (test run, lint, build, etc.).
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
status:
|
|
127
|
+
``"passed"``, ``"failed"``, or ``"skipped"``.
|
|
128
|
+
command:
|
|
129
|
+
The shell command that was executed (e.g. ``"pytest"``).
|
|
130
|
+
output:
|
|
131
|
+
Truncated output from the command.
|
|
132
|
+
"""
|
|
133
|
+
global _ledger
|
|
134
|
+
_ledger["verification_events"].append({
|
|
135
|
+
"time": time.time(),
|
|
136
|
+
"status": status,
|
|
137
|
+
"command": command,
|
|
138
|
+
"output_summary": output[:500] if output else "",
|
|
139
|
+
})
|
|
140
|
+
if status == "passed":
|
|
141
|
+
_ledger["verified"] = True
|
|
142
|
+
_ledger["last_verify_time"] = time.time()
|
|
143
|
+
_ledger["verify_attempts"] += 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def reset_ledger() -> None:
|
|
147
|
+
"""Reset the verification ledger (e.g. at session start)."""
|
|
148
|
+
global _ledger
|
|
149
|
+
_ledger = {
|
|
150
|
+
"changed_paths": [],
|
|
151
|
+
"verification_events": [],
|
|
152
|
+
"verified": False,
|
|
153
|
+
"last_edit_time": 0.0,
|
|
154
|
+
"last_verify_time": 0.0,
|
|
155
|
+
"verify_attempts": 0,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Non-code path filter (same heuristic as Hermes' verification_stop.py)
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
_NON_CODE_EXTENSIONS: frozenset[str] = frozenset({
|
|
164
|
+
".md",
|
|
165
|
+
".markdown",
|
|
166
|
+
".mdx",
|
|
167
|
+
".rst",
|
|
168
|
+
".txt",
|
|
169
|
+
".text",
|
|
170
|
+
".adoc",
|
|
171
|
+
".asciidoc",
|
|
172
|
+
".org",
|
|
173
|
+
".log",
|
|
174
|
+
".csv",
|
|
175
|
+
".tsv",
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
_NON_CODE_FILENAMES: frozenset[str] = frozenset({
|
|
179
|
+
"license",
|
|
180
|
+
"licence",
|
|
181
|
+
"notice",
|
|
182
|
+
"authors",
|
|
183
|
+
"contributors",
|
|
184
|
+
"changelog",
|
|
185
|
+
"codeowners",
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _is_non_code_path(raw: str) -> bool:
|
|
190
|
+
"""Return True when a file path is documentation/prose with nothing to verify."""
|
|
191
|
+
try:
|
|
192
|
+
p = Path(str(raw))
|
|
193
|
+
except Exception:
|
|
194
|
+
return False
|
|
195
|
+
suffix = p.suffix.lower()
|
|
196
|
+
if suffix in _NON_CODE_EXTENSIONS:
|
|
197
|
+
return True
|
|
198
|
+
if not suffix and p.name.lower() in _NON_CODE_FILENAMES:
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _filter_verifiable_paths(paths: list[str]) -> list[str]:
|
|
204
|
+
"""Drop documentation/prose paths; keep code paths that need verification."""
|
|
205
|
+
return [p for p in paths if p and not _is_non_code_path(p)]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Nudge builder
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
_MAX_CHANGED_PATHS_SHOWN = 8
|
|
213
|
+
_MAX_VERIFY_ATTEMPTS = 2
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _format_paths(paths: list[str]) -> str:
|
|
217
|
+
"""Pretty-print changed paths for the nudge message."""
|
|
218
|
+
shown = paths[:_MAX_CHANGED_PATHS_SHOWN]
|
|
219
|
+
lines = [f"- `{p}`" for p in shown]
|
|
220
|
+
remaining = len(paths) - len(shown)
|
|
221
|
+
if remaining > 0:
|
|
222
|
+
lines.append(f"- ... and {remaining} more")
|
|
223
|
+
return "\n".join(lines)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_verify_nudge() -> str | None:
|
|
227
|
+
"""Build a verification nudge message if code edits need fresh verification.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
str or None
|
|
232
|
+
The nudge text (wrapped in ``[System: ...]`` markers), or None when no
|
|
233
|
+
nudge is needed (no edits, only doc files, already verified, or
|
|
234
|
+
max attempts reached).
|
|
235
|
+
"""
|
|
236
|
+
global _ledger
|
|
237
|
+
|
|
238
|
+
paths = sorted(
|
|
239
|
+
{str(p) for p in _filter_verifiable_paths(_ledger["changed_paths"])}
|
|
240
|
+
)
|
|
241
|
+
if not paths:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
if _ledger["verify_attempts"] >= _MAX_VERIFY_ATTEMPTS:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
if _ledger["verified"] and _ledger["last_verify_time"] >= _ledger["last_edit_time"]:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
# Build status summary from the latest verification event
|
|
251
|
+
detail_parts: list[str] = []
|
|
252
|
+
if _ledger["verification_events"]:
|
|
253
|
+
last = _ledger["verification_events"][-1]
|
|
254
|
+
state = last.get("status", "unverified")
|
|
255
|
+
detail_parts.append(state)
|
|
256
|
+
cmd = last.get("command", "")
|
|
257
|
+
if cmd:
|
|
258
|
+
detail_parts.append(f"last command `{cmd}`")
|
|
259
|
+
output = last.get("output_summary", "")
|
|
260
|
+
if output:
|
|
261
|
+
max_output = 1200
|
|
262
|
+
if len(output) > max_output:
|
|
263
|
+
output = output[:max_output].rstrip() + "\n... [truncated]"
|
|
264
|
+
detail_parts.append(f"last output:\n{output}")
|
|
265
|
+
else:
|
|
266
|
+
detail_parts.append("unverified")
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
"[System: You edited code in this turn, but the workspace does not have "
|
|
270
|
+
"fresh passing verification evidence yet.\n\n"
|
|
271
|
+
f"Verification status: {' | '.join(detail_parts)}\n\n"
|
|
272
|
+
f"Changed paths:\n{_format_paths(paths)}\n\n"
|
|
273
|
+
"Run the relevant verification command now (test, lint, build), "
|
|
274
|
+
"read any failure, repair the code, and summarize what passed. "
|
|
275
|
+
"If verification is not possible, explain the concrete blocker "
|
|
276
|
+
"instead of claiming the work is fully verified.]"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def verify_status() -> dict[str, Any]:
|
|
281
|
+
"""Return the current verification status as a dictionary.
|
|
282
|
+
|
|
283
|
+
Use this from MCP tools to let agents query verification state.
|
|
284
|
+
"""
|
|
285
|
+
global _ledger
|
|
286
|
+
paths = sorted(
|
|
287
|
+
{str(p) for p in _filter_verifiable_paths(_ledger["changed_paths"])}
|
|
288
|
+
)
|
|
289
|
+
return {
|
|
290
|
+
"changed_paths": paths,
|
|
291
|
+
"verification_events": _ledger["verification_events"][-3:],
|
|
292
|
+
"verified": _ledger["verified"],
|
|
293
|
+
"attempts": _ledger["verify_attempts"],
|
|
294
|
+
"needs_verification": bool(paths) and not _ledger["verified"],
|
|
295
|
+
"nudge": build_verify_nudge(),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
__all__ = [
|
|
300
|
+
"detect_host_agent",
|
|
301
|
+
"host_has_verify_on_stop",
|
|
302
|
+
"record_edit",
|
|
303
|
+
"record_verification",
|
|
304
|
+
"reset_ledger",
|
|
305
|
+
"build_verify_nudge",
|
|
306
|
+
"verify_status",
|
|
307
|
+
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eling
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: Unified second brain for AI agents — 5-tier memory, HRR reasoning,
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Unified second brain for AI agents — 5-tier memory, HRR reasoning, 10 MCP tools, conditional verify-on-stop
|
|
5
5
|
Author: PatrickNoFilter
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: memory,mcp,ai-agent,second-brain,hrr
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
# 🧠 Eling
|
|
30
30
|
|
|
31
|
-
**Unified second brain for AI agents — 5-tier memory, HRR reasoning,
|
|
31
|
+
**Unified second brain for AI agents — 5-tier memory, HRR reasoning, 10 MCP tools, conditional verify-on-stop**
|
|
32
32
|
|
|
33
33
|
*"Eling" (Javanese): to remember, to be conscious, to be aware*
|
|
34
34
|
|
|
@@ -66,6 +66,7 @@ All accessible via **9 MCP tools** from a single stdio server:
|
|
|
66
66
|
| `eling_stats` | Show per-layer statistics |
|
|
67
67
|
| `eling_think` | Synthesis + gap analysis across layers |
|
|
68
68
|
| `eling_export` | Full brain export as JSON or Markdown |
|
|
69
|
+
| `eling_verify` | Query/record verification status (conditional) |
|
|
69
70
|
|
|
70
71
|
## 🚀 Quick Start
|
|
71
72
|
|
|
@@ -200,6 +201,52 @@ print(result) # {"layer": "notion", "page_id": "...", ...}
|
|
|
200
201
|
|
|
201
202
|
> **Note**: `eling_reflect` and `remember(layer="notion")` check availability at call time and return a clear error if any config is missing — no silent failures.
|
|
202
203
|
|
|
204
|
+
## 🛡️ Verify-on-Stop (Conditional)
|
|
205
|
+
|
|
206
|
+
Eling provides **verify-on-stop** nudges for AI agents that lack built-in
|
|
207
|
+
verification (e.g., OpenCode, OpenClaw, Cursor, Windsurf). When running under
|
|
208
|
+
Hermes, this feature automatically **skips** — because Hermes already has its
|
|
209
|
+
own `agent/verification_stop.py`.
|
|
210
|
+
|
|
211
|
+
### How it works
|
|
212
|
+
|
|
213
|
+
1. **Auto-detection** — Eling detects the host agent from environment variables
|
|
214
|
+
(`HERMES_SESSION_SOURCE` → Hermes, `OPENCODE_HOME` → OpenCode, etc.)
|
|
215
|
+
2. **File edit tracking** — When code files are edited via hooks or MCP tools,
|
|
216
|
+
eling records them in a verification ledger
|
|
217
|
+
3. **Verification nudge** — If code was edited but no passing tests/verification
|
|
218
|
+
was recorded, eling produces a `[System: ...]` nudge message
|
|
219
|
+
4. **Recording** — Agents can call `eling_verify` MCP tool to record verification
|
|
220
|
+
results (`passed`, `failed`, `skipped`)
|
|
221
|
+
|
|
222
|
+
### Usage via MCP
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
// Query current status
|
|
226
|
+
{ "method": "tools/call", "params": { "name": "eling_verify", "arguments": {} } }
|
|
227
|
+
|
|
228
|
+
// Record a passing verification
|
|
229
|
+
{ "method": "tools/call", "params": {
|
|
230
|
+
"name": "eling_verify",
|
|
231
|
+
"arguments": { "status": "passed", "command": "pytest", "output": "364 passed" }
|
|
232
|
+
} }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Config
|
|
236
|
+
|
|
237
|
+
| Key | Default | Env | Description |
|
|
238
|
+
|-----|---------|-----|-------------|
|
|
239
|
+
| `verify_on_stop` | `true` | `ELING_VERIFY_ON_STOP` | Enable nudges for non-Hermes agents |
|
|
240
|
+
| `verify_on_stop_max_attempts` | `2` | `ELING_VERIFY_MAX_ATTEMPTS` | Max nudges per session |
|
|
241
|
+
| `adapter` | `hermes` | `ELING_ADAPTER` | Force adapter type |
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
plugins:
|
|
245
|
+
eling:
|
|
246
|
+
adapter: auto # auto-detect from env
|
|
247
|
+
verify_on_stop: true
|
|
248
|
+
```
|
|
249
|
+
|
|
203
250
|
## 🏗️ Architecture
|
|
204
251
|
|
|
205
252
|
```
|
|
@@ -14,6 +14,7 @@ src/eling/mcp_server.py
|
|
|
14
14
|
src/eling/permissions.py
|
|
15
15
|
src/eling/privacy.py
|
|
16
16
|
src/eling/snapshot.py
|
|
17
|
+
src/eling/verify_on_stop.py
|
|
17
18
|
src/eling.egg-info/PKG-INFO
|
|
18
19
|
src/eling.egg-info/SOURCES.txt
|
|
19
20
|
src/eling.egg-info/dependency_links.txt
|
|
@@ -49,4 +50,5 @@ tests/test_privacy.py
|
|
|
49
50
|
tests/test_schema_packs.py
|
|
50
51
|
tests/test_snapshot.py
|
|
51
52
|
tests/test_sync.py
|
|
52
|
-
tests/test_think.py
|
|
53
|
+
tests/test_think.py
|
|
54
|
+
tests/test_verify_on_stop.py
|
|
@@ -72,7 +72,7 @@ class TestExportMCP:
|
|
|
72
72
|
|
|
73
73
|
def test_nine_tools_total(self):
|
|
74
74
|
from eling.mcp_server import TOOLS
|
|
75
|
-
assert len(TOOLS) ==
|
|
75
|
+
assert len(TOOLS) == 10 # eling_remember..eling_verify
|
|
76
76
|
|
|
77
77
|
def test_export_covers_all_layers(self, brain):
|
|
78
78
|
"""Full JSON export covers facts, entity_graph, kb, code, notion, builtin."""
|
|
@@ -127,8 +127,8 @@ class TestHookRegistry:
|
|
|
127
127
|
# ============================================================================
|
|
128
128
|
|
|
129
129
|
class TestAllHooks:
|
|
130
|
-
def
|
|
131
|
-
assert len(ALL_HOOKS) == 15
|
|
130
|
+
def test_exactly_16_hooks(self):
|
|
131
|
+
assert len(ALL_HOOKS) == 16 # was 15, now 16 with verify_request
|
|
132
132
|
|
|
133
133
|
def test_all_hooks_are_strings(self):
|
|
134
134
|
for h in ALL_HOOKS:
|
|
@@ -138,7 +138,8 @@ class TestAllHooks:
|
|
|
138
138
|
required = {
|
|
139
139
|
"session_start", "pre_user_message", "post_user_message",
|
|
140
140
|
"pre_tool_use", "post_tool_use", "post_assistant_message",
|
|
141
|
-
"decision_made", "file_edit", "
|
|
141
|
+
"decision_made", "file_edit", "verify_request",
|
|
142
|
+
"error_occurred",
|
|
142
143
|
"compaction", "session_end", "idle_30min",
|
|
143
144
|
"sync_start", "sync_complete", "sync_error",
|
|
144
145
|
}
|
|
@@ -157,7 +158,7 @@ class TestBuiltinHandlers:
|
|
|
157
158
|
|
|
158
159
|
def test_default_hooks_are_registered(self, brain):
|
|
159
160
|
"""Brain.__init__ registers all default hooks."""
|
|
160
|
-
assert brain.hooks.total_handlers == 15
|
|
161
|
+
assert brain.hooks.total_handlers == 16 # 15 + verify_request
|
|
161
162
|
for hook in ALL_HOOKS:
|
|
162
163
|
assert brain.hooks.has_handlers(hook), f"Missing handler for {hook}"
|
|
163
164
|
|
|
@@ -343,12 +344,12 @@ class TestBrainStatsHooks:
|
|
|
343
344
|
brain = Brain(home=tmp)
|
|
344
345
|
s = brain.stats()
|
|
345
346
|
assert "hooks" in s
|
|
346
|
-
assert s["hooks"]["total_handlers"] == 15
|
|
347
|
-
assert s["hooks"]["hooks_with_handlers"] ==
|
|
347
|
+
assert s["hooks"]["total_handlers"] == 16 # 15 + verify_request
|
|
348
|
+
assert s["hooks"]["hooks_with_handlers"] == 16
|
|
348
349
|
|
|
349
350
|
def test_stats_reflects_custom_hook(self):
|
|
350
351
|
tmp = Path(tempfile.mkdtemp())
|
|
351
352
|
brain = Brain(home=tmp)
|
|
352
353
|
brain.hooks.register(HOOK_SESSION_START, lambda n, c: None)
|
|
353
354
|
s = brain.stats()
|
|
354
|
-
assert s["hooks"]["total_handlers"] == 16
|
|
355
|
+
assert s["hooks"]["total_handlers"] == 17 # 16 base + 1 custom
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Tests for eling.verify_on_stop — conditional verify-on-stop."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from eling import verify_on_stop as vos
|
|
9
|
+
from eling.brain import Brain
|
|
10
|
+
from eling.hooks import HOOK_VERIFY_REQUEST, HOOK_FILE_EDIT
|
|
11
|
+
|
|
12
|
+
# ── Detection tests ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestDetectHostAgent:
|
|
16
|
+
def test_detects_hermes_from_env(self):
|
|
17
|
+
with patch.dict(os.environ, {"HERMES_SESSION_SOURCE": "tui"}, clear=True):
|
|
18
|
+
assert vos.detect_host_agent() == "hermes"
|
|
19
|
+
|
|
20
|
+
def test_detects_opencode_from_env(self):
|
|
21
|
+
with patch.dict(os.environ, {"OPENCODE_HOME": "/root/.opencode"}, clear=True):
|
|
22
|
+
assert vos.detect_host_agent() == "opencode"
|
|
23
|
+
|
|
24
|
+
def test_detects_generic_with_no_env(self):
|
|
25
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
26
|
+
assert vos.detect_host_agent() == "generic"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestHostHasVerifyOnStop:
|
|
30
|
+
def test_hermes_adapter_has_verify(self):
|
|
31
|
+
assert vos.host_has_verify_on_stop(adapter="hermes") is True
|
|
32
|
+
|
|
33
|
+
def test_opencode_adapter_lacks_verify(self):
|
|
34
|
+
assert vos.host_has_verify_on_stop(adapter="opencode") is False
|
|
35
|
+
|
|
36
|
+
def test_openclaw_adapter_lacks_verify(self):
|
|
37
|
+
assert vos.host_has_verify_on_stop(adapter="openclaw") is False
|
|
38
|
+
|
|
39
|
+
def test_auto_detects_hermes_from_env(self):
|
|
40
|
+
with patch.dict(os.environ, {"HERMES_SESSION_SOURCE": "tui"}, clear=True):
|
|
41
|
+
assert vos.host_has_verify_on_stop(adapter="auto") is True
|
|
42
|
+
|
|
43
|
+
def test_auto_detects_opencode_from_env(self):
|
|
44
|
+
with patch.dict(os.environ, {"OPENCODE_HOME": "/root/.opencode"}, clear=True):
|
|
45
|
+
assert vos.host_has_verify_on_stop(adapter="auto") is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Ledger tests ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestVerificationLedger:
|
|
52
|
+
def setup_method(self):
|
|
53
|
+
vos.reset_ledger()
|
|
54
|
+
|
|
55
|
+
def test_empty_ledger_no_nudge(self):
|
|
56
|
+
assert vos.build_verify_nudge() is None
|
|
57
|
+
assert vos.verify_status()["needs_verification"] is False
|
|
58
|
+
|
|
59
|
+
def test_record_edit_then_needs_verification(self):
|
|
60
|
+
vos.record_edit("src/main.py")
|
|
61
|
+
status = vos.verify_status()
|
|
62
|
+
assert "src/main.py" in status["changed_paths"]
|
|
63
|
+
assert status["needs_verification"] is True
|
|
64
|
+
|
|
65
|
+
def test_record_verification_passed_clears_nudge(self):
|
|
66
|
+
vos.record_edit("src/main.py")
|
|
67
|
+
assert vos.build_verify_nudge() is not None
|
|
68
|
+
vos.record_verification(status="passed", command="pytest")
|
|
69
|
+
assert vos.build_verify_nudge() is None
|
|
70
|
+
|
|
71
|
+
def test_max_attempts_exhausted(self):
|
|
72
|
+
vos.record_edit("src/main.py")
|
|
73
|
+
vos.record_verification(status="failed", command="pytest", output="1 failed")
|
|
74
|
+
vos.record_verification(status="failed", command="pytest", output="1 failed")
|
|
75
|
+
# Third call hits max_attempts=2
|
|
76
|
+
vos.record_verification(status="failed", command="pytest", output="1 failed")
|
|
77
|
+
assert vos.build_verify_nudge() is None
|
|
78
|
+
|
|
79
|
+
def test_non_code_paths_filtered(self):
|
|
80
|
+
vos.record_edit("README.md")
|
|
81
|
+
vos.record_edit("LICENSE")
|
|
82
|
+
assert vos.build_verify_nudge() is None
|
|
83
|
+
status = vos.verify_status()
|
|
84
|
+
assert status["changed_paths"] == []
|
|
85
|
+
|
|
86
|
+
def test_mixed_code_and_docs(self):
|
|
87
|
+
vos.record_edit("src/main.py")
|
|
88
|
+
vos.record_edit("README.md")
|
|
89
|
+
assert "src/main.py" in vos.verify_status()["changed_paths"]
|
|
90
|
+
assert "README.md" not in vos.verify_status()["changed_paths"]
|
|
91
|
+
|
|
92
|
+
def test_reset_ledger(self):
|
|
93
|
+
vos.record_edit("src/main.py")
|
|
94
|
+
assert vos.build_verify_nudge() is not None
|
|
95
|
+
vos.reset_ledger()
|
|
96
|
+
assert vos.build_verify_nudge() is None
|
|
97
|
+
|
|
98
|
+
def test_nudge_format_contains_paths(self):
|
|
99
|
+
vos.record_edit("src/main.py")
|
|
100
|
+
nudge = vos.build_verify_nudge()
|
|
101
|
+
assert nudge is not None
|
|
102
|
+
assert "src/main.py" in nudge
|
|
103
|
+
assert "[System:" in nudge
|
|
104
|
+
assert "Verification status" in nudge
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Brain integration tests ──────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestBrainVerifyMethod:
|
|
111
|
+
def test_verify_hermes_adapter_skips(self):
|
|
112
|
+
b = Brain(adapter="hermes")
|
|
113
|
+
result = b.verify()
|
|
114
|
+
assert result["host_has_verify"] is True
|
|
115
|
+
assert result["active"] is False
|
|
116
|
+
|
|
117
|
+
def test_verify_opencode_adapter_active(self):
|
|
118
|
+
b = Brain(adapter="opencode")
|
|
119
|
+
result = b.verify()
|
|
120
|
+
assert result["host_has_verify"] is False
|
|
121
|
+
assert result["active"] is True
|
|
122
|
+
|
|
123
|
+
def test_verify_record_passed(self):
|
|
124
|
+
b = Brain(adapter="opencode")
|
|
125
|
+
result = b.verify(status="passed", command="pytest")
|
|
126
|
+
assert result["recorded"] is True
|
|
127
|
+
assert result["status"] == "passed"
|
|
128
|
+
|
|
129
|
+
def test_verify_query_after_edit(self):
|
|
130
|
+
b = Brain(adapter="opencode")
|
|
131
|
+
b.fire_hook(HOOK_FILE_EDIT, file_path="src/main.py")
|
|
132
|
+
result = b.verify()
|
|
133
|
+
assert result["active"] is True
|
|
134
|
+
assert len(result["changed_paths"]) >= 1
|
|
135
|
+
assert result["needs_verification"] is True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── MCP tool registration ────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestVerifyMCPTool:
|
|
142
|
+
def test_verify_tool_in_tools_list(self):
|
|
143
|
+
from eling.mcp_server import TOOLS
|
|
144
|
+
|
|
145
|
+
names = [t["name"] for t in TOOLS]
|
|
146
|
+
assert "eling_verify" in names
|
|
147
|
+
|
|
148
|
+
def test_verify_tool_accepts_status_param(self):
|
|
149
|
+
from eling.mcp_server import TOOLS
|
|
150
|
+
|
|
151
|
+
verify_def = [t for t in TOOLS if t["name"] == "eling_verify"][0]
|
|
152
|
+
props = verify_def["inputSchema"]["properties"]
|
|
153
|
+
assert "status" in props
|
|
154
|
+
assert props["status"]["enum"] == ["", "passed", "failed", "skipped"]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ── Config tests ─────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestVerifyConfig:
|
|
161
|
+
def test_verify_on_stop_in_defaults(self):
|
|
162
|
+
from eling.config import DEFAULTS
|
|
163
|
+
|
|
164
|
+
assert "verify_on_stop" in DEFAULTS
|
|
165
|
+
assert DEFAULTS["verify_on_stop"] is True
|
|
166
|
+
|
|
167
|
+
def test_verify_max_attempts_in_defaults(self):
|
|
168
|
+
from eling.config import DEFAULTS
|
|
169
|
+
|
|
170
|
+
assert "verify_on_stop_max_attempts" in DEFAULTS
|
|
171
|
+
assert DEFAULTS["verify_on_stop_max_attempts"] == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|