@oussamadouhou/agent-enforcement 0.1.2

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 (40) hide show
  1. package/.claude-plugin/plugin.json +5 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/logger.d.ts +23 -0
  4. package/dist/adapters/logger.d.ts.map +1 -0
  5. package/dist/adapters/logger.js +58 -0
  6. package/dist/adapters/logger.js.map +1 -0
  7. package/dist/adapters/message-reader.d.ts +20 -0
  8. package/dist/adapters/message-reader.d.ts.map +1 -0
  9. package/dist/adapters/message-reader.js +123 -0
  10. package/dist/adapters/message-reader.js.map +1 -0
  11. package/dist/constants.d.ts +22 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +50 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/detection/environment.d.ts +7 -0
  16. package/dist/detection/environment.d.ts.map +1 -0
  17. package/dist/detection/environment.js +30 -0
  18. package/dist/detection/environment.js.map +1 -0
  19. package/dist/hook.d.ts +35 -0
  20. package/dist/hook.d.ts.map +1 -0
  21. package/dist/hook.js +223 -0
  22. package/dist/hook.js.map +1 -0
  23. package/dist/index.d.ts +14 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/plugin.d.ts +4 -0
  28. package/dist/plugin.d.ts.map +1 -0
  29. package/dist/plugin.js +6 -0
  30. package/dist/plugin.js.map +1 -0
  31. package/dist/schema/verification-schema.d.ts +127 -0
  32. package/dist/schema/verification-schema.d.ts.map +1 -0
  33. package/dist/schema/verification-schema.js +132 -0
  34. package/dist/schema/verification-schema.js.map +1 -0
  35. package/docs/EVIDENCE_SYSTEM.md +214 -0
  36. package/fixtures/claude/project-abc/session-123.jsonl +2 -0
  37. package/fixtures/opencode/messages/session-123/msg_001.json +4 -0
  38. package/hooks/enforcement.py +255 -0
  39. package/hooks/hooks.json +15 -0
  40. package/package.json +70 -0
@@ -0,0 +1,214 @@
1
+ # Evidence-Based Trust System
2
+
3
+ **Version**: 1.0.0
4
+ **Plugin**: @oussamadouhou/agent-enforcement
5
+
6
+ ---
7
+
8
+ ## Overview
9
+
10
+ The Evidence-Based Trust System prevents AI agents from marking tasks complete without providing verifiable evidence of completion.
11
+
12
+ ### Purpose
13
+
14
+ - **Prevent premature completion**: No marking todos done without proof
15
+ - **Enforce accountability**: Every completion requires evidence
16
+ - **Enable verification**: Evidence must be independently verifiable
17
+ - **Build trust**: Ground truth over assumptions
18
+
19
+ ---
20
+
21
+ ## Enforcement Levels
22
+
23
+ Set via environment variable: `OPENCODE_ENFORCEMENT_LEVEL`
24
+
25
+ | Level | Value | Behavior |
26
+ |-------|-------|----------|
27
+ | **CREATIVE** | `0` | No enforcement (brainstorming, planning) |
28
+ | **STANDARD** | `1` | Warning in tool output (default) |
29
+ | **STRICT** | `2` | Block tool execution with error |
30
+
31
+ **Set level:**
32
+ ```bash
33
+ export OPENCODE_ENFORCEMENT_LEVEL=1 # 0, 1, or 2
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Required Evidence Format
39
+
40
+ Before marking any todo as `completed`, you **MUST** provide an evidence block:
41
+
42
+ ```markdown
43
+ **Evidence for [todo-id]**:
44
+ **Execution**: [command/tool used]
45
+ **Verification**: [what was checked]
46
+ **Checklist**:
47
+ - [x] Tool executed successfully
48
+ - [x] Output captured
49
+ - [x] Result matches expected
50
+ - [x] No workarounds used
51
+ - [x] Independently verifiable
52
+ **Trust**: 🟢 HIGH | ✅ Ground Truth
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Checklist Items (All Required)
58
+
59
+ ### 1. Tool executed successfully
60
+ - ✅ Tool returned exit code 0 (bash)
61
+ - ✅ Tool completed without errors
62
+ - ✅ Output was generated
63
+ - ❌ NOT: "I think it worked"
64
+
65
+ ### 2. Output captured
66
+ - ✅ Tool output included in response
67
+ - ✅ Output is readable and complete
68
+ - ✅ Output shown to user
69
+ - ❌ NOT: Hidden or summarized
70
+
71
+ ### 3. Result matches expected
72
+ - ✅ Output contains expected data
73
+ - ✅ Changes are visible in files/system
74
+ - ✅ Verification command confirms success
75
+ - ❌ NOT: Assumed without checking
76
+
77
+ ### 4. No workarounds used
78
+ - ✅ Proper solution implemented
79
+ - ✅ No hacks or temporary fixes
80
+ - ✅ Follows best practices
81
+ - ❌ NOT: `as any`, ignoring errors, deleting tests
82
+
83
+ ### 5. Independently verifiable
84
+ - ✅ Another person/agent can verify
85
+ - ✅ Evidence is objective
86
+ - ✅ Not dependent on hidden state
87
+ - ❌ NOT: "Trust me"
88
+
89
+ ---
90
+
91
+ ## Trust Markers (Required)
92
+
93
+ ### Confidence Level
94
+ - 🟢 **HIGH**: Direct observation, ground truth
95
+ - 🟡 **MEDIUM**: Indirect verification, inference
96
+ - 🔴 **LOW**: Assumption, no verification
97
+
98
+ ### Evidence Type
99
+ - ✅ **Ground Truth**: Tool executed, output captured
100
+ - ⚠️ **Simulation**: Mental model, not executed
101
+ - ❌ **Assumption**: No verification attempted
102
+
103
+ ---
104
+
105
+ ## Examples
106
+
107
+ ### ✅ GOOD: Complete Evidence
108
+
109
+ ```markdown
110
+ I'll check the working directory.
111
+
112
+ **Evidence for todo-1**:
113
+ **Execution**: `pwd` command via bash tool
114
+ **Verification**: Output shows `/home/user/project`
115
+ **Checklist**:
116
+ - [x] Tool executed successfully (exit code 0)
117
+ - [x] Output captured: `/home/user/project`
118
+ - [x] Result matches expected (correct directory)
119
+ - [x] No workarounds used (standard pwd command)
120
+ - [x] Independently verifiable (anyone can run pwd)
121
+ **Trust**: 🟢 HIGH | ✅ Ground Truth
122
+
123
+ *Now marking todo-1 complete*
124
+ ```
125
+
126
+ ### ❌ BAD: No Evidence
127
+
128
+ ```markdown
129
+ I checked the working directory.
130
+
131
+ *Marking todo-1 complete* ← VIOLATION
132
+ ```
133
+
134
+ ### ❌ BAD: Incomplete Checklist
135
+
136
+ ```markdown
137
+ **Evidence for todo-1**:
138
+ **Execution**: `pwd`
139
+ **Verification**: It worked
140
+ **Checklist**:
141
+ - [x] Tool executed
142
+ - [ ] Output captured ← MISSING
143
+ **Trust**: 🟢 HIGH
144
+ ```
145
+
146
+ ---
147
+
148
+ ## When Evidence is NOT Required
149
+
150
+ - ❌ Todo status is `pending`, `in_progress`, `cancelled`
151
+ - ✅ Only `completed` status requires evidence
152
+
153
+ ---
154
+
155
+ ## Violations
156
+
157
+ ### What Happens (STANDARD Mode)
158
+
159
+ When you try to mark a todo complete without evidence:
160
+
161
+ 1. **Tool completes normally** (todo is updated)
162
+ 2. **Warning appears in tool output** (you see it in response)
163
+ 3. **You should acknowledge** and provide evidence before proceeding
164
+
165
+ ### What Happens (STRICT Mode)
166
+
167
+ When you try to mark a todo complete without evidence:
168
+
169
+ 1. **Tool execution is BLOCKED** (error thrown)
170
+ 2. **Todo is NOT updated**
171
+ 3. **You MUST provide evidence** then retry
172
+
173
+ ---
174
+
175
+ ## FAQ
176
+
177
+ ### Q: Why is this necessary?
178
+ **A**: Prevents agents from claiming tasks are done without proof, improves reliability and accountability.
179
+
180
+ ### Q: Can I disable this?
181
+ **A**: Set `OPENCODE_ENFORCEMENT_LEVEL=0` for CREATIVE mode (no enforcement).
182
+
183
+ ### Q: What if I'm just planning?
184
+ **A**: Use CREATIVE mode (level 0) for planning/brainstorming.
185
+
186
+ ### Q: Can I provide evidence after completion?
187
+ **A**: No - evidence must be provided BEFORE marking complete.
188
+
189
+ ### Q: What counts as valid evidence?
190
+ **A**: Tool execution output, file contents, command results - anything independently verifiable.
191
+
192
+ ---
193
+
194
+ ## Technical Details
195
+
196
+ ### Hook Points
197
+ - `tool.execute.before`: Validation logic
198
+ - `tool.execute.after`: Warning injection
199
+
200
+ ### Detection
201
+ - Monitors `todowrite` tool calls
202
+ - Checks last assistant message for evidence block
203
+ - Validates checklist items and trust markers
204
+
205
+ ### Warning Injection
206
+ - Appends `<system-reminder>` to tool output
207
+ - LLM sees warning in tool response
208
+ - Human sees toast notification in TUI
209
+
210
+ ---
211
+
212
+ **Documentation Location**: `~/.config/opencode/plugins/agent-enforcement/docs/EVIDENCE_SYSTEM.md`
213
+ **Plugin Repository**: https://github.com/oussamadouhou/agent-enforcement
214
+ **Issues**: https://github.com/oussamadouhou/agent-enforcement/issues
@@ -0,0 +1,2 @@
1
+ {"role":"user","content":[{"type":"text","text":"Run the check"}]}
2
+ {"role":"assistant","content":[{"type":"text","text":"**Evidence for task-1**:\n\n**Execution**:\n- Tool: read\n\n**Verification**:\n- Checked output\n\n**Checklist**:\n- [x] Tool executed successfully\n- [x] Output captured\n- [x] Result matches expected\n- [x] No workarounds used\n- [x] Independently verifiable\n\n**Trust**: 🟢 HIGH | ✅ Ground Truth"}]}
@@ -0,0 +1,4 @@
1
+ {
2
+ "role": "assistant",
3
+ "text": "**Evidence for task-1**:\n\n**Execution**:\n- Tool: read\n\n**Verification**:\n- Checked output\n\n**Checklist**:\n- [x] Tool executed successfully\n- [x] Output captured\n- [x] Result matches expected\n- [x] No workarounds used\n- [x] Independently verifiable\n\n**Trust**: 🟢 HIGH | ✅ Ground Truth"
4
+ }
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Claude Code hook handler for evidence enforcement.
4
+ Reads stdin (hook input), validates evidence, outputs JSON result.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ HOOK_NAME = "evidence-enforcement"
17
+
18
+ ENFORCEMENT_LEVELS = {
19
+ "CREATIVE": 0,
20
+ "STANDARD": 1,
21
+ "STRICT": 2,
22
+ }
23
+
24
+ EVIDENCE_PATTERNS = {
25
+ "evidence_block": re.compile(r"\*\*Evidence(?:\s+for\s+[\w-]+)?\*\*:", re.IGNORECASE),
26
+ "execution_section": re.compile(r"\*\*Execution\*\*:", re.IGNORECASE),
27
+ "verification_section": re.compile(r"\*\*Verification\*\*:", re.IGNORECASE),
28
+ "checklist_section": re.compile(r"\*\*Checklist\*\*:", re.IGNORECASE),
29
+ "trust_markers": re.compile(r"\*\*(?:Confidence|Trust)\*\*:", re.IGNORECASE),
30
+ }
31
+
32
+ REQUIRED_CHECKLIST_ITEMS = [
33
+ "Tool executed",
34
+ "Output captured",
35
+ "Result matches",
36
+ "No workarounds",
37
+ "Independently verifiable",
38
+ ]
39
+
40
+
41
+ class Logger:
42
+ def __init__(self, mode: str, path: str, level: str) -> None:
43
+ self.mode = mode
44
+ self.path = path
45
+ self.level = level
46
+ self.level_map = {"debug": 10, "info": 20, "warn": 30, "error": 40}
47
+
48
+ def _should_log(self, level: str) -> bool:
49
+ if self.mode == "silent":
50
+ return False
51
+ return self.level_map.get(level, 20) >= self.level_map.get(self.level, 20)
52
+
53
+ def _log(self, level: str, msg: str) -> None:
54
+ if not self._should_log(level):
55
+ return
56
+ timestamp = datetime.utcnow().isoformat()
57
+ formatted = f"[{timestamp}] [{level.upper()}] {msg}"
58
+ if self.mode == "file":
59
+ try:
60
+ with open(self.path, "a", encoding="utf-8") as handle:
61
+ handle.write(formatted + "\n")
62
+ except Exception:
63
+ return
64
+ elif self.mode == "console":
65
+ print(formatted)
66
+
67
+ def info(self, msg: str) -> None:
68
+ self._log("info", msg)
69
+
70
+ def warn(self, msg: str) -> None:
71
+ self._log("warn", msg)
72
+
73
+ def error(self, msg: str) -> None:
74
+ self._log("error", msg)
75
+
76
+ def debug(self, msg: str) -> None:
77
+ self._log("debug", msg)
78
+
79
+
80
+ def get_enforcement_level() -> int:
81
+ env_level = os.environ.get("ENFORCEMENT_LEVEL") or os.environ.get("OPENCODE_ENFORCEMENT_LEVEL")
82
+ if not env_level:
83
+ return ENFORCEMENT_LEVELS["STANDARD"]
84
+ if env_level.isdigit():
85
+ parsed = int(env_level)
86
+ if parsed in (0, 1, 2):
87
+ return parsed
88
+ upper = env_level.upper()
89
+ return ENFORCEMENT_LEVELS.get(upper, ENFORCEMENT_LEVELS["STANDARD"])
90
+
91
+
92
+ def get_log_config() -> Dict[str, str]:
93
+ return {
94
+ "mode": os.environ.get("ENFORCEMENT_LOG_MODE", "file"),
95
+ "path": os.environ.get("ENFORCEMENT_LOG_PATH", "/tmp/agent-enforcement.log"),
96
+ "level": os.environ.get("ENFORCEMENT_LOG_LEVEL", "info"),
97
+ }
98
+
99
+
100
+ def extract_text(message: Dict[str, Any]) -> Optional[str]:
101
+ text = message.get("text")
102
+ if isinstance(text, str):
103
+ return text
104
+ content = message.get("content")
105
+ if isinstance(content, list):
106
+ for part in content:
107
+ if isinstance(part, dict) and part.get("type") == "text":
108
+ part_text = part.get("text")
109
+ if isinstance(part_text, str):
110
+ return part_text
111
+ return None
112
+
113
+
114
+ def read_last_assistant_message(session_id: str) -> Optional[str]:
115
+ projects_dir = Path.home() / ".claude" / "projects"
116
+ if not projects_dir.exists():
117
+ return None
118
+
119
+ for project_dir in projects_dir.iterdir():
120
+ if not project_dir.is_dir():
121
+ continue
122
+ session_file = project_dir / f"{session_id}.jsonl"
123
+ if not session_file.exists():
124
+ continue
125
+ try:
126
+ lines = session_file.read_text(encoding="utf-8").splitlines()
127
+ except Exception:
128
+ continue
129
+ for line in reversed(lines):
130
+ try:
131
+ message = json.loads(line)
132
+ except json.JSONDecodeError:
133
+ continue
134
+ if message.get("role") == "assistant":
135
+ return extract_text(message) or line
136
+ return None
137
+
138
+
139
+ def validate_message_text(message_text: str) -> Tuple[bool, List[str]]:
140
+ missing_items: List[str] = []
141
+ for item in REQUIRED_CHECKLIST_ITEMS:
142
+ item_regex = re.compile(rf"\[\s*[xX]\s*\].*{re.escape(item)}", re.IGNORECASE)
143
+ if not item_regex.search(message_text):
144
+ missing_items.append(item)
145
+
146
+ has_evidence_block = bool(EVIDENCE_PATTERNS["evidence_block"].search(message_text))
147
+ has_trust_markers = bool(EVIDENCE_PATTERNS["trust_markers"].search(message_text))
148
+
149
+ if not has_evidence_block:
150
+ return False, ["Evidence block missing"]
151
+ if missing_items:
152
+ return False, missing_items
153
+ if not has_trust_markers:
154
+ return False, ["Trust markers missing"]
155
+ return True, []
156
+
157
+
158
+ def build_error_message(missing: List[str], level: int) -> str:
159
+ status = "BLOCKED" if level == ENFORCEMENT_LEVELS["STRICT"] else "WARNING"
160
+ if "Evidence block missing" in missing:
161
+ return (
162
+ f"[{HOOK_NAME}] {status}: Cannot mark todo complete without evidence block.\n\n"
163
+ "Required format:\n"
164
+ "**Evidence for [todo-id]**:\n"
165
+ "**Execution**: [command/tool used]\n"
166
+ "**Verification**: [what was checked]\n"
167
+ "**Checklist**:\n"
168
+ "- [x] Tool executed successfully\n"
169
+ "- [x] Output captured\n"
170
+ "- [x] Result matches expected\n"
171
+ "- [x] No workarounds used\n"
172
+ "- [x] Independently verifiable\n"
173
+ "**Trust**: 🟢 HIGH | ✅ Ground Truth\n\n"
174
+ "See ~/AGENTS.md \"Evidence-Based Trust System\" for details."
175
+ )
176
+ if "Trust markers missing" in missing:
177
+ return (
178
+ f"[{HOOK_NAME}] {status}: Trust markers missing.\n\n"
179
+ "Required:\n"
180
+ "**Confidence**: 🟢 HIGH | 🟡 MEDIUM | 🔴 LOW\n"
181
+ "**Evidence**: ✅ Ground Truth | ⚠️ Simulation | ❌ Assumption"
182
+ )
183
+ checklist = "\n".join([f"- [ ] {item}" for item in missing])
184
+ return (
185
+ f"[{HOOK_NAME}] {status}: Evidence checklist incomplete.\n\n"
186
+ f"Missing checklist items:\n{checklist}\n\n"
187
+ "All items must be checked [x] before marking todo complete."
188
+ )
189
+
190
+
191
+ def parse_todos(raw_todos: Any) -> List[Dict[str, Any]]:
192
+ if isinstance(raw_todos, list):
193
+ return [t for t in raw_todos if isinstance(t, dict)]
194
+ if isinstance(raw_todos, str):
195
+ try:
196
+ parsed = json.loads(raw_todos)
197
+ if isinstance(parsed, list):
198
+ return [t for t in parsed if isinstance(t, dict)]
199
+ except json.JSONDecodeError:
200
+ return []
201
+ return []
202
+
203
+
204
+ def main() -> None:
205
+ input_data = json.loads(sys.stdin.read())
206
+ tool = input_data.get("tool", "")
207
+ session_id = input_data.get("sessionID", "")
208
+ args = input_data.get("args", {}) if isinstance(input_data.get("args"), dict) else {}
209
+ raw_todos = args.get("todos", [])
210
+
211
+ if tool != "todowrite":
212
+ print(json.dumps({"action": "allow"}))
213
+ return
214
+
215
+ level = get_enforcement_level()
216
+ log_config = get_log_config()
217
+ logger = Logger(**log_config)
218
+
219
+ if level == ENFORCEMENT_LEVELS["CREATIVE"]:
220
+ logger.info(f"[{HOOK_NAME}] CREATIVE mode - no enforcement")
221
+ print(json.dumps({"action": "allow"}))
222
+ return
223
+
224
+ todos = parse_todos(raw_todos)
225
+ completed = [t for t in todos if t.get("status") == "completed"]
226
+ if not completed:
227
+ print(json.dumps({"action": "allow"}))
228
+ return
229
+
230
+ last_message = read_last_assistant_message(session_id)
231
+ if not last_message:
232
+ message = build_error_message(["Evidence block missing"], level)
233
+ if level == ENFORCEMENT_LEVELS["STRICT"]:
234
+ print(json.dumps({"action": "block", "message": message}))
235
+ else:
236
+ logger.warn(message)
237
+ print(json.dumps({"action": "allow"}))
238
+ return
239
+
240
+ valid, missing = validate_message_text(last_message)
241
+ if valid:
242
+ logger.info(f"[{HOOK_NAME}] Evidence validation passed")
243
+ print(json.dumps({"action": "allow"}))
244
+ return
245
+
246
+ message = build_error_message(missing, level)
247
+ if level == ENFORCEMENT_LEVELS["STRICT"]:
248
+ print(json.dumps({"action": "block", "message": message}))
249
+ else:
250
+ logger.warn(message)
251
+ print(json.dumps({"action": "allow"}))
252
+
253
+
254
+ if __name__ == "__main__":
255
+ main()
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/enforcement.py",
9
+ "timeout": 10
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@oussamadouhou/agent-enforcement",
3
+ "version": "0.1.2",
4
+ "description": "Evidence enforcement hooks for OpenCode and Claude Code",
5
+ "main": "dist/plugin.js",
6
+ "type": "module",
7
+ "types": "dist/plugin.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/plugin.d.ts",
11
+ "import": "./dist/plugin.js",
12
+ "default": "./dist/plugin.js"
13
+ },
14
+ "./index": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./schema": {
20
+ "types": "./dist/schema/verification-schema.d.ts",
21
+ "import": "./dist/schema/verification-schema.js",
22
+ "default": "./dist/schema/verification-schema.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "hooks",
28
+ "docs",
29
+ ".claude-plugin",
30
+ "fixtures"
31
+ ],
32
+ "scripts": {
33
+ "build": "bun run clean && bun run compile",
34
+ "clean": "rm -rf dist",
35
+ "compile": "bunx tsc",
36
+ "prepublishOnly": "bun run build",
37
+ "test": "bun test"
38
+ },
39
+ "keywords": [
40
+ "opencode",
41
+ "claude-code",
42
+ "enforcement",
43
+ "evidence",
44
+ "todo"
45
+ ],
46
+ "author": "Oussama Douhou",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/oussamadouhou/agent-enforcement.git"
51
+ },
52
+ "homepage": "https://github.com/oussamadouhou/agent-enforcement#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/oussamadouhou/agent-enforcement/issues"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "peerDependencies": {
60
+ "@opencode-ai/plugin": "^1.1.21"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^20.0.0",
64
+ "typescript": "^5.0.0",
65
+ "bun-types": "^1.0.0"
66
+ },
67
+ "engines": {
68
+ "node": ">=18.0.0"
69
+ }
70
+ }