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.
Files changed (56) hide show
  1. {eling-0.2.1/src/eling.egg-info → eling-0.2.2}/PKG-INFO +50 -3
  2. {eling-0.2.1 → eling-0.2.2}/README.md +48 -1
  3. {eling-0.2.1 → eling-0.2.2}/pyproject.toml +2 -2
  4. {eling-0.2.1 → eling-0.2.2}/src/eling/__init__.py +9 -4
  5. {eling-0.2.1 → eling-0.2.2}/src/eling/brain.py +39 -0
  6. {eling-0.2.1 → eling-0.2.2}/src/eling/config.py +8 -0
  7. {eling-0.2.1 → eling-0.2.2}/src/eling/hooks.py +55 -8
  8. {eling-0.2.1 → eling-0.2.2}/src/eling/mcp_server.py +37 -0
  9. eling-0.2.2/src/eling/verify_on_stop.py +307 -0
  10. {eling-0.2.1 → eling-0.2.2/src/eling.egg-info}/PKG-INFO +50 -3
  11. {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/SOURCES.txt +3 -1
  12. {eling-0.2.1 → eling-0.2.2}/tests/test_export.py +1 -1
  13. {eling-0.2.1 → eling-0.2.2}/tests/test_hooks.py +8 -7
  14. {eling-0.2.1 → eling-0.2.2}/tests/test_think.py +1 -1
  15. eling-0.2.2/tests/test_verify_on_stop.py +171 -0
  16. {eling-0.2.1 → eling-0.2.2}/LICENSE +0 -0
  17. {eling-0.2.1 → eling-0.2.2}/setup.cfg +0 -0
  18. {eling-0.2.1 → eling-0.2.2}/src/eling/adapters/__init__.py +0 -0
  19. {eling-0.2.1 → eling-0.2.2}/src/eling/cli.py +0 -0
  20. {eling-0.2.1 → eling-0.2.2}/src/eling/compress.py +0 -0
  21. {eling-0.2.1 → eling-0.2.2}/src/eling/decay.py +0 -0
  22. {eling-0.2.1 → eling-0.2.2}/src/eling/export.py +0 -0
  23. {eling-0.2.1 → eling-0.2.2}/src/eling/hermes_plugin.py +0 -0
  24. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/__init__.py +0 -0
  25. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/builtin.py +0 -0
  26. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/code.py +0 -0
  27. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/code_index.py +0 -0
  28. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/facts.py +0 -0
  29. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/hrr.py +0 -0
  30. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/kb.py +0 -0
  31. {eling-0.2.1 → eling-0.2.2}/src/eling/layers/notion.py +0 -0
  32. {eling-0.2.1 → eling-0.2.2}/src/eling/permissions.py +0 -0
  33. {eling-0.2.1 → eling-0.2.2}/src/eling/privacy.py +0 -0
  34. {eling-0.2.1 → eling-0.2.2}/src/eling/scripts/benchmark.py +0 -0
  35. {eling-0.2.1 → eling-0.2.2}/src/eling/snapshot.py +0 -0
  36. {eling-0.2.1 → eling-0.2.2}/src/eling/utils/__init__.py +0 -0
  37. {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/dependency_links.txt +0 -0
  38. {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/requires.txt +0 -0
  39. {eling-0.2.1 → eling-0.2.2}/src/eling.egg-info/top_level.txt +0 -0
  40. {eling-0.2.1 → eling-0.2.2}/tests/test_adapters.py +0 -0
  41. {eling-0.2.1 → eling-0.2.2}/tests/test_brain.py +0 -0
  42. {eling-0.2.1 → eling-0.2.2}/tests/test_builtin.py +0 -0
  43. {eling-0.2.1 → eling-0.2.2}/tests/test_cli.py +0 -0
  44. {eling-0.2.1 → eling-0.2.2}/tests/test_compress.py +0 -0
  45. {eling-0.2.1 → eling-0.2.2}/tests/test_config.py +0 -0
  46. {eling-0.2.1 → eling-0.2.2}/tests/test_contradiction.py +0 -0
  47. {eling-0.2.1 → eling-0.2.2}/tests/test_decay.py +0 -0
  48. {eling-0.2.1 → eling-0.2.2}/tests/test_facts.py +0 -0
  49. {eling-0.2.1 → eling-0.2.2}/tests/test_graph.py +0 -0
  50. {eling-0.2.1 → eling-0.2.2}/tests/test_hrr.py +0 -0
  51. {eling-0.2.1 → eling-0.2.2}/tests/test_kb.py +0 -0
  52. {eling-0.2.1 → eling-0.2.2}/tests/test_permissions.py +0 -0
  53. {eling-0.2.1 → eling-0.2.2}/tests/test_privacy.py +0 -0
  54. {eling-0.2.1 → eling-0.2.2}/tests/test_schema_packs.py +0 -0
  55. {eling-0.2.1 → eling-0.2.2}/tests/test_snapshot.py +0 -0
  56. {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.1
4
- Summary: Unified second brain for AI agents — 5-tier memory, HRR reasoning, 9 MCP tools
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, 9 MCP tools**
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, 9 MCP tools**
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.1"
8
- description = "Unified second brain for AI agents — 5-tier memory, HRR reasoning, 9 MCP tools"
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.1"
7
- __all__ = ["Brain", "HookRegistry", "ALL_HOOKS", "register_default_hooks",
8
- "remember", "recall", "reason", "resolve_config", "set_config_key",
9
- "get_config", "describe_config"]
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 layer."""
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 or not brain.code.available:
264
- return {"reindexed": False}
265
- try:
266
- brain.code.reindex(file_path)
267
- return {"reindexed": True, "file": file_path}
268
- except Exception:
269
- return {"reindexed": False}
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.1
4
- Summary: Unified second brain for AI agents — 5-tier memory, HRR reasoning, 9 MCP tools
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, 9 MCP tools**
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) == 9 # was 8, now 9 with eling_export
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 test_exactly_15_hooks(self):
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", "error_occurred",
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"] == 15
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
@@ -106,4 +106,4 @@ class TestThinkMCPTools:
106
106
 
107
107
  def test_nine_tools_total(self):
108
108
  from eling.mcp_server import TOOLS
109
- assert len(TOOLS) == 9 # was 8, now 9 with eling_export
109
+ assert len(TOOLS) == 10 # eling_remember..eling_verify
@@ -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