Skip to content

Commit e57f29f

Browse files
authored
Merge pull request #209 from notque/feat/perses-plugin-example
feat: add Perses plugin scaffolds, hooks, and agent-comparison autoresearch
2 parents 250c351 + 9fdc523 commit e57f29f

43 files changed

Lines changed: 4634 additions & 292 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@
9090
"command": "python3 \"$HOME/.claude/hooks/anti-rationalization-injector.py\"",
9191
"description": "Inject anti-rationalization warnings based on task-type keywords",
9292
"timeout": 1000
93+
},
94+
{
95+
"type": "command",
96+
"command": "python3 \"$HOME/.claude/hooks/creation-request-enforcer-userprompt.py\"",
97+
"description": "Early ADR enforcement: detect creation requests before model processing begins",
98+
"timeout": 5000
9399
}
94100
]
95101
}
@@ -297,6 +303,16 @@
297303
"timeout": 2000
298304
}
299305
]
306+
},
307+
{
308+
"matcher": "Write|Edit",
309+
"hooks": [
310+
{
311+
"type": "command",
312+
"command": "python3 ~/.claude/hooks/sql-injection-detector.py",
313+
"timeout": 5000
314+
}
315+
]
300316
}
301317
],
302318
"PreCompact": [

agents/INDEX.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"agents": {
55
"agent-creator-engineer": {
66
"file": "agent-creator-engineer.md",
7-
"short_description": "**DEPRECATED**: Use skill-creator skill instead",
7+
"short_description": "**DEPRECATED**: Use skill-creator agent instead",
88
"triggers": [
99
"create agent",
1010
"new agent",
@@ -107,10 +107,7 @@
107107
"programming rules"
108108
],
109109
"pairs_with": [
110-
"github-profile-rules-repo-analysis",
111-
"github-profile-rules-pr-review",
112-
"github-profile-rules-synthesis",
113-
"github-profile-rules-validation"
110+
"github-profile-rules"
114111
],
115112
"complexity": "Medium",
116113
"category": "meta"
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python3
2+
# hook-version: 1.0.0
3+
"""
4+
UserPromptSubmit Hook: Creation Request ADR Enforcer
5+
6+
Fires at UserPromptSubmit time — BEFORE the model begins processing — and checks
7+
whether the user's prompt contains creation keywords. If a creation request is
8+
detected without a recent ADR session, it injects a strong context message
9+
reminding Claude that an ADR is mandatory before any other action.
10+
11+
This hook complements the PreToolUse:Agent creation-protocol-enforcer.py by
12+
catching the requirement earlier in the pipeline, before routing has occurred.
13+
14+
Allow-through conditions:
15+
- No creation keywords found in prompt
16+
- .adr-session.json exists and was modified within the last 900 seconds
17+
- ADR_PROTOCOL_BYPASS=1 env var is set
18+
"""
19+
20+
import json
21+
import os
22+
import sys
23+
import time
24+
import traceback
25+
from pathlib import Path
26+
27+
sys.path.insert(0, str(Path(__file__).parent / "lib"))
28+
from hook_utils import context_output, empty_output
29+
from stdin_timeout import read_stdin
30+
31+
_BYPASS_ENV = "ADR_PROTOCOL_BYPASS"
32+
_ADR_SESSION_FILE = ".adr-session.json"
33+
_STALENESS_THRESHOLD_SECONDS = 900
34+
_EVENT_NAME = "UserPromptSubmit"
35+
36+
_CREATION_KEYWORDS = [
37+
"create",
38+
"scaffold",
39+
"build a new",
40+
"build a ",
41+
"add a new",
42+
"add new",
43+
"new agent",
44+
"new skill",
45+
"new pipeline",
46+
"new hook",
47+
"new feature",
48+
"new workflow",
49+
"new plugin",
50+
"implement new",
51+
"i need a ",
52+
"i need an ",
53+
"we need a ",
54+
"we need an ",
55+
]
56+
57+
_WARNING_TEXT = """\
58+
[creation-enforcer] CREATION REQUEST DETECTED — ADR IS MANDATORY BEFORE ANY OTHER ACTION
59+
60+
You MUST complete these steps BEFORE dispatching any agent or writing any files:
61+
1. Write ADR at adr/{name}.md (use kebab-case name describing what you're creating)
62+
2. Register: python3 scripts/adr-query.py register --adr adr/{name}.md
63+
3. Only THEN proceed to routing and agent dispatch.
64+
65+
Skipping this step will be blocked by the pretool-adr-creation-gate hook.\
66+
"""
67+
68+
69+
def _has_creation_keywords(prompt: str) -> bool:
70+
"""Return True if the prompt contains any creation keyword (case-insensitive)."""
71+
lower = prompt.lower()
72+
return any(kw in lower for kw in _CREATION_KEYWORDS)
73+
74+
75+
def _adr_session_is_recent(base_dir: Path) -> bool:
76+
"""Return True if .adr-session.json exists and was modified within the threshold."""
77+
adr_session_path = base_dir / _ADR_SESSION_FILE
78+
if not adr_session_path.exists():
79+
return False
80+
try:
81+
mtime = os.path.getmtime(adr_session_path)
82+
age = time.time() - mtime
83+
return age <= _STALENESS_THRESHOLD_SECONDS
84+
except OSError:
85+
return False
86+
87+
88+
def main() -> None:
89+
"""Run the UserPromptSubmit creation enforcement check."""
90+
debug = os.environ.get("CLAUDE_HOOKS_DEBUG")
91+
92+
raw = read_stdin(timeout=2)
93+
try:
94+
event = json.loads(raw)
95+
except (json.JSONDecodeError, ValueError):
96+
empty_output(_EVENT_NAME).print_and_exit()
97+
98+
# Bypass env var.
99+
if os.environ.get(_BYPASS_ENV) == "1":
100+
if debug:
101+
print(
102+
f"[creation-enforcer] Bypassed via {_BYPASS_ENV}=1",
103+
file=sys.stderr,
104+
)
105+
empty_output(_EVENT_NAME).print_and_exit()
106+
107+
# UserPromptSubmit event uses the "prompt" field for the user message.
108+
prompt = event.get("prompt", "") if isinstance(event, dict) else ""
109+
if not prompt:
110+
empty_output(_EVENT_NAME).print_and_exit()
111+
112+
# Check for creation keywords.
113+
if not _has_creation_keywords(prompt):
114+
if debug:
115+
print(
116+
"[creation-enforcer] No creation keywords found — allowing through",
117+
file=sys.stderr,
118+
)
119+
empty_output(_EVENT_NAME).print_and_exit()
120+
121+
# Resolve project root.
122+
cwd_str = event.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR", ".")
123+
base_dir = Path(cwd_str).resolve()
124+
125+
# Check whether a recent ADR session exists.
126+
if _adr_session_is_recent(base_dir):
127+
if debug:
128+
print(
129+
"[creation-enforcer] Recent .adr-session.json found — allowing through",
130+
file=sys.stderr,
131+
)
132+
empty_output(_EVENT_NAME).print_and_exit()
133+
134+
if debug:
135+
print(
136+
"[creation-enforcer] Creation keywords found, no recent ADR session — injecting warning",
137+
file=sys.stderr,
138+
)
139+
140+
# No recent ADR session — inject strong advisory context.
141+
context_output(_EVENT_NAME, _WARNING_TEXT).print_and_exit()
142+
143+
144+
if __name__ == "__main__":
145+
try:
146+
main()
147+
except SystemExit:
148+
raise
149+
except Exception as e:
150+
if os.environ.get("CLAUDE_HOOKS_DEBUG"):
151+
traceback.print_exc(file=sys.stderr)
152+
else:
153+
print(
154+
f"[creation-enforcer] Error: {type(e).__name__}: {e}",
155+
file=sys.stderr,
156+
)
157+
# Fail open — never exit non-zero on unexpected errors.
158+
sys.exit(0)

hooks/lib/learning_db_v2.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
_DEFAULT_DB_DIR = Path.home() / ".claude" / "learning"
3030

31-
_CURRENT_SCHEMA_VERSION = 2
31+
_CURRENT_SCHEMA_VERSION = 3
3232

3333
CATEGORY_DEFAULTS = {
3434
"error": 0.55,
@@ -132,6 +132,26 @@ def _run_migrations(conn: sqlite3.Connection) -> None:
132132
"VALUES (2, 'add graduation_proposed_at column to learnings')"
133133
)
134134

135+
if current < 3:
136+
# v2 -> v3: Add performance indexes for timestamp range queries and ROI cohort scans
137+
for ddl in (
138+
"CREATE INDEX IF NOT EXISTS idx_learnings_last_seen ON learnings(last_seen)",
139+
"CREATE INDEX IF NOT EXISTS idx_learnings_first_seen ON learnings(first_seen)",
140+
"CREATE INDEX IF NOT EXISTS idx_sessions_start_time ON sessions(start_time)",
141+
"CREATE INDEX IF NOT EXISTS idx_activations_timestamp ON activations(timestamp)",
142+
"CREATE INDEX IF NOT EXISTS idx_session_stats_had_retro ON session_stats(had_retro_knowledge)",
143+
"CREATE INDEX IF NOT EXISTS idx_session_stats_created_at ON session_stats(created_at)",
144+
):
145+
try:
146+
conn.execute(ddl)
147+
except sqlite3.OperationalError:
148+
pass # Index already exists
149+
conn.execute("PRAGMA user_version = 3")
150+
conn.execute(
151+
"INSERT OR IGNORE INTO schema_migrations (version, description) "
152+
"VALUES (3, 'add timestamp and cohort indexes for query performance')"
153+
)
154+
135155
conn.commit()
136156

137157

@@ -235,7 +255,10 @@ def _migrate_fts(pre_migration_version: int = 0) -> None:
235255
CREATE INDEX IF NOT EXISTS idx_learnings_project ON learnings(project_path);
236256
CREATE INDEX IF NOT EXISTS idx_learnings_graduated ON learnings(graduated_to);
237257
CREATE INDEX IF NOT EXISTS idx_learnings_error_sig ON learnings(error_signature);
258+
CREATE INDEX IF NOT EXISTS idx_learnings_last_seen ON learnings(last_seen);
259+
CREATE INDEX IF NOT EXISTS idx_learnings_first_seen ON learnings(first_seen);
238260
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
261+
CREATE INDEX IF NOT EXISTS idx_sessions_start_time ON sessions(start_time);
239262
240263
CREATE VIRTUAL TABLE IF NOT EXISTS learnings_fts USING fts5(
241264
topic,
@@ -267,7 +290,10 @@ def _migrate_fts(pre_migration_version: int = 0) -> None:
267290
268291
CREATE INDEX IF NOT EXISTS idx_activations_topic_key ON activations(topic, key);
269292
CREATE INDEX IF NOT EXISTS idx_activations_session ON activations(session_id);
293+
CREATE INDEX IF NOT EXISTS idx_activations_timestamp ON activations(timestamp);
270294
CREATE INDEX IF NOT EXISTS idx_session_stats_session ON session_stats(session_id);
295+
CREATE INDEX IF NOT EXISTS idx_session_stats_had_retro ON session_stats(had_retro_knowledge);
296+
CREATE INDEX IF NOT EXISTS idx_session_stats_created_at ON session_stats(created_at);
271297
272298
CREATE TRIGGER IF NOT EXISTS learnings_ai AFTER INSERT ON learnings BEGIN
273299
INSERT INTO learnings_fts(rowid, topic, key, value, tags)

hooks/lib/usage_db.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def init_db():
7777
CREATE INDEX IF NOT EXISTS idx_agent_type ON agent_invocations(agent_type);
7878
CREATE INDEX IF NOT EXISTS idx_skill_ts ON skill_invocations(timestamp);
7979
CREATE INDEX IF NOT EXISTS idx_agent_ts ON agent_invocations(timestamp);
80+
CREATE INDEX IF NOT EXISTS idx_skill_name_ts ON skill_invocations(skill_name, timestamp);
81+
CREATE INDEX IF NOT EXISTS idx_agent_type_ts ON agent_invocations(agent_type, timestamp);
8082
""")
8183
conn.commit()
8284

0 commit comments

Comments
 (0)