Skip to content

Commit 6abf7b3

Browse files
committed
Add lifecycle hooks and agent context for BookStore project
- Introduced lifecycle hooks for PreToolUse, SessionStart, PreCompact, Stop, SubagentStart, and SubagentStop events. - Implemented scripts for code rule checks, security checks, memory protocol enforcement, session context injection, and build validation. - Created JSON configuration files for each hook to define their behavior and associated scripts. - Added an audit log script to track user prompts for observability. - Updated documentation to include details about the new GitHub Copilot agent team and lifecycle hooks. - Enhanced .gitignore to exclude local audit logs.
1 parent 9ed560a commit 6abf7b3

17 files changed

Lines changed: 1146 additions & 1 deletion

.github/hooks/audit.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"hooks": {
3+
"UserPromptSubmit": [
4+
{
5+
"type": "command",
6+
"command": "python3 .github/hooks/scripts/audit_prompt.py",
7+
"windows": "py -3 .github\\hooks\\scripts\\audit_prompt.py",
8+
"timeout": 5
9+
}
10+
]
11+
}
12+
}

.github/hooks/code-rules.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"type": "command",
6+
"command": "python3 .github/hooks/scripts/check_code_rules.py",
7+
"windows": "py -3 .github\\hooks\\scripts\\check_code_rules.py",
8+
"timeout": 10
9+
}
10+
]
11+
}
12+
}

.github/hooks/memory-protocol.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"type": "command",
6+
"command": "python3 .github/hooks/scripts/check_memory_protocol.py",
7+
"windows": "py -3 .github\\hooks\\scripts\\check_memory_protocol.py",
8+
"timeout": 5
9+
}
10+
]
11+
}
12+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python3
2+
"""
3+
UserPromptSubmit hook: Append every prompt to a local audit log.
4+
5+
Non-blocking — always exits 0. Used for observability only.
6+
The log file is gitignored and stays local to the developer's machine.
7+
8+
Log location: .github/hooks/logs/audit.log
9+
10+
Input (stdin): VS Code UserPromptSubmit JSON payload
11+
Output (stdout): nothing (non-blocking)
12+
"""
13+
14+
import json
15+
import os
16+
import sys
17+
from datetime import datetime, timezone
18+
19+
LOG_PATH = os.path.join(".github", "hooks", "logs", "audit.log")
20+
21+
22+
def main() -> None:
23+
try:
24+
data = json.load(sys.stdin)
25+
except Exception:
26+
sys.exit(0)
27+
28+
prompt: str = data.get("prompt", "").strip()
29+
session_id: str = data.get("sessionId", "unknown")
30+
timestamp: str = datetime.now(timezone.utc).isoformat()
31+
32+
os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
33+
34+
try:
35+
with open(LOG_PATH, "a", encoding="utf-8") as f:
36+
f.write(f"[{timestamp}] session={session_id}\n{prompt}\n---\n")
37+
except Exception:
38+
pass # Never fail the hook on a logging error
39+
40+
41+
if __name__ == "__main__":
42+
main()
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
"""
3+
PreToolUse hook: Block edits containing BookStore banned patterns.
4+
5+
Scans .cs files in edit tool calls and denies the operation when
6+
any AGENTS.md code rule violation is detected.
7+
8+
Input (stdin): VS Code PreToolUse JSON payload
9+
Output (stdout): JSON deny decision, or nothing on success
10+
"""
11+
12+
import json
13+
import re
14+
import sys
15+
16+
# (regex_pattern, human-readable fix message)
17+
RULES: list[tuple[str, str]] = [
18+
(
19+
r"\bGuid\.NewGuid\(\)",
20+
"Use Guid.CreateVersion7() instead of Guid.NewGuid()",
21+
),
22+
(
23+
r"\bDateTime\.Now\b",
24+
"Use DateTimeOffset.UtcNow instead of DateTime.Now",
25+
),
26+
(
27+
r"\b_logger\.Log(?:Information|Warning|Error|Debug|Critical|Trace)\s*\(",
28+
"Use [LoggerMessage] source generator — never call _logger.Log*() directly",
29+
),
30+
(
31+
r"namespace\s+[\w.]+\s*\{",
32+
"Use file-scoped namespaces: 'namespace BookStore.X;' not 'namespace BookStore.X { }'",
33+
),
34+
(
35+
r'"(?:\*DEFAULT\*|default)"',
36+
'Use MultiTenancyConstants.* instead of hardcoded tenant strings "*DEFAULT*" or "default"',
37+
),
38+
]
39+
40+
41+
def check_content(content: str) -> list[str]:
42+
return [msg for pattern, msg in RULES if re.search(pattern, content)]
43+
44+
45+
def extract_cs_files(tool_input: dict) -> list[tuple[str, str]]:
46+
"""Return (path, content) pairs for .cs files, handling multiple tool input shapes."""
47+
results: list[tuple[str, str]] = []
48+
49+
# editFiles / createFile shape: { files: [{filePath, content}] }
50+
for f in tool_input.get("files", []):
51+
if isinstance(f, dict):
52+
path = f.get("filePath", f.get("path", ""))
53+
content = f.get("content", f.get("newContent", ""))
54+
else:
55+
path, content = str(f), ""
56+
if path.endswith(".cs") and content:
57+
results.append((path, content))
58+
59+
# replaceStringInFile shape: { filePath, newString }
60+
fp = tool_input.get("filePath", "")
61+
if fp.endswith(".cs"):
62+
new_str = tool_input.get("newString", "")
63+
if new_str:
64+
results.append((fp, new_str))
65+
66+
return results
67+
68+
69+
def deny(reason: str) -> None:
70+
output = {
71+
"hookSpecificOutput": {
72+
"hookEventName": "PreToolUse",
73+
"permissionDecision": "deny",
74+
"permissionDecisionReason": reason,
75+
}
76+
}
77+
print(json.dumps(output))
78+
sys.exit(0)
79+
80+
81+
def main() -> None:
82+
try:
83+
data = json.load(sys.stdin)
84+
except Exception:
85+
sys.exit(0)
86+
87+
cs_files = extract_cs_files(data.get("tool_input", {}))
88+
if not cs_files:
89+
sys.exit(0)
90+
91+
all_violations: list[str] = []
92+
for path, content in cs_files:
93+
for violation in check_content(content):
94+
all_violations.append(f" • {path}: {violation}")
95+
96+
if all_violations:
97+
deny("BookStore code rule violations:\n" + "\n".join(all_violations))
98+
99+
100+
if __name__ == "__main__":
101+
main()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python3
2+
"""
3+
PreToolUse hook: Enforce the agent memory handoff protocol.
4+
5+
Agents may only write to the six designated /memories/session/ files.
6+
Writes to user memory (/memories/) or repo memory (/memories/repo/) are
7+
blocked to prevent agents corrupting persistent state.
8+
9+
Input (stdin): VS Code PreToolUse JSON payload
10+
Output (stdout): JSON deny decision, or nothing on success
11+
"""
12+
13+
import json
14+
import sys
15+
16+
ALLOWED_FILES: frozenset[str] = frozenset(
17+
{
18+
"task-brief.md",
19+
"plan.md",
20+
"backend-output.md",
21+
"frontend-output.md",
22+
"test-output.md",
23+
"review.md",
24+
}
25+
)
26+
27+
WRITE_COMMANDS: frozenset[str] = frozenset({"create", "str_replace", "insert", "delete"})
28+
29+
30+
def deny(reason: str) -> None:
31+
output = {
32+
"hookSpecificOutput": {
33+
"hookEventName": "PreToolUse",
34+
"permissionDecision": "deny",
35+
"permissionDecisionReason": reason,
36+
}
37+
}
38+
print(json.dumps(output))
39+
sys.exit(0)
40+
41+
42+
def main() -> None:
43+
try:
44+
data = json.load(sys.stdin)
45+
except Exception:
46+
sys.exit(0)
47+
48+
tool_name: str = data.get("tool_name", "").lower()
49+
if "memory" not in tool_name:
50+
sys.exit(0)
51+
52+
tool_input: dict = data.get("tool_input", {})
53+
command: str = tool_input.get("command", "")
54+
55+
# Read / view operations are always allowed
56+
if command not in WRITE_COMMANDS:
57+
sys.exit(0)
58+
59+
path: str = tool_input.get("path", tool_input.get("old_path", ""))
60+
61+
# Must be under /memories/session/
62+
if not path.startswith("/memories/session/"):
63+
deny(
64+
f"Memory protocol violation: agents may only write to /memories/session/.\n"
65+
f" Attempted path : {path}\n"
66+
f" Allowed prefix : /memories/session/\n"
67+
f" Allowed files : {', '.join(sorted(ALLOWED_FILES))}\n\n"
68+
f"Writing to user memory (/memories/) or repo memory (/memories/repo/) "
69+
f"requires explicit user intent — do not do this from an agent."
70+
)
71+
72+
# Must be one of the designated handoff filenames
73+
filename = path.rstrip("/").split("/")[-1]
74+
if filename not in ALLOWED_FILES:
75+
deny(
76+
f"Memory protocol violation: '{filename}' is not a recognised session handoff file.\n"
77+
f" Attempted path : {path}\n"
78+
f" Allowed files : {', '.join(sorted(ALLOWED_FILES))}"
79+
)
80+
81+
82+
if __name__ == "__main__":
83+
main()

0 commit comments

Comments
 (0)