Skip to content

Commit fcca3e8

Browse files
committed
add skills
1 parent e674057 commit fcca3e8

13 files changed

Lines changed: 1776 additions & 431 deletions

File tree

.github/agents/maintainer.agent.md

Lines changed: 115 additions & 431 deletions
Large diffs are not rendered by default.

.github/agents/reviewer.agent.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ Automated reviews consistently miss:
3737
- Localization gaps in user-facing messages
3838
- Accessibility regressions
3939

40+
## Related Skills
41+
42+
For deep-dive patterns, these skills provide additional context:
43+
44+
| Skill | Use When |
45+
| ----------------------- | ------------------------------- |
46+
| `/cross-platform-paths` | Reviewing path-related code |
47+
| `/settings-precedence` | Reviewing settings code |
48+
| `/manager-discovery` | Reviewing manager-specific code |
49+
50+
The patterns below are the essential subset needed during reviews.
51+
4052
---
4153

4254
## Review Process
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"description": "Agent hooks for vscode-python-environments maintainer workflow",
4+
"hooks": {
5+
"SessionStart": [
6+
{
7+
"type": "command",
8+
"command": "python .github/hooks/scripts/session_start.py",
9+
"windows": "python .github\\hooks\\scripts\\session_start.py",
10+
"timeout": 10,
11+
"env": {
12+
"PYTHONPATH": "."
13+
}
14+
}
15+
],
16+
"PostToolUse": [
17+
{
18+
"type": "command",
19+
"command": "python .github/hooks/scripts/post_tool_use.py",
20+
"windows": "python .github\\hooks\\scripts\\post_tool_use.py",
21+
"timeout": 60,
22+
"env": {
23+
"PYTHONPATH": "."
24+
}
25+
}
26+
],
27+
"Stop": [
28+
{
29+
"type": "command",
30+
"command": "python .github/hooks/scripts/stop_hook.py",
31+
"windows": "python .github\\hooks\\scripts\\stop_hook.py",
32+
"timeout": 15,
33+
"env": {
34+
"PYTHONPATH": "."
35+
}
36+
}
37+
],
38+
"SubagentStop": [
39+
{
40+
"type": "command",
41+
"command": "python .github/hooks/scripts/subagent_stop.py",
42+
"windows": "python .github\\hooks\\scripts\\subagent_stop.py",
43+
"timeout": 10,
44+
"env": {
45+
"PYTHONPATH": "."
46+
}
47+
}
48+
]
49+
}
50+
}

.github/hooks/scripts/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copilot Agent Hooks Scripts
2+
3+
This directory contains Python scripts used by agent hooks to automate workflow validation.
4+
5+
## Scripts
6+
7+
### session_start.py
8+
9+
Runs at session start to inject project context:
10+
11+
- Current git branch and commit
12+
- Uncommitted changes count
13+
- Open issues (if gh CLI available)
14+
- Snapshot health summary (if available)
15+
- Available skills reminder
16+
17+
### post_tool_use.py
18+
19+
Runs after file edit tools to provide immediate feedback:
20+
21+
- Runs ESLint on changed TypeScript files
22+
- Reports lint errors back to the model
23+
24+
### stop_hook.py
25+
26+
Runs before session ends to enforce workflow:
27+
28+
- Checks for uncommitted TypeScript changes
29+
- Reminds about pre-commit checks
30+
- Blocks completion if staged changes aren't committed
31+
32+
### subagent_stop.py
33+
34+
Runs when subagents complete:
35+
36+
- Currently a passthrough for logging
37+
- Can be extended to validate reviewer output
38+
39+
## Requirements
40+
41+
These scripts use Python 3.10+ with no external dependencies (beyond what's already in the repo).
42+
43+
They expect:
44+
45+
- `git` CLI available
46+
- `gh` CLI available (optional, for issue context)
47+
- `npx` available for running ESLint
48+
49+
## Hook Configuration
50+
51+
See `.github/hooks/maintainer-hooks.json` for the hook configuration that loads these scripts.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
"""PostToolUse hook - Runs validation after file edits.
5+
6+
After editFiles tool:
7+
- Runs ESLint on changed TypeScript files
8+
- Reports lint errors back to the model as additional context
9+
"""
10+
11+
import json
12+
import os
13+
import subprocess
14+
import sys
15+
from pathlib import Path
16+
17+
# Tools that modify files and should trigger validation
18+
FILE_EDIT_TOOLS = {"editFiles", "createFile", "create_file", "replace_string_in_file"}
19+
20+
# File patterns to validate
21+
TYPESCRIPT_EXTENSIONS = {".ts", ".tsx"}
22+
23+
24+
def run_eslint(files: list[str], cwd: Path) -> str | None:
25+
"""Run ESLint on specified files and return errors."""
26+
ts_files = [f for f in files if Path(f).suffix in TYPESCRIPT_EXTENSIONS]
27+
if not ts_files:
28+
return None
29+
30+
try:
31+
result = subprocess.run(
32+
["npx", "eslint", "--format", "compact", *ts_files],
33+
cwd=cwd,
34+
capture_output=True,
35+
text=True,
36+
timeout=30,
37+
)
38+
if result.returncode != 0 and result.stdout:
39+
# Parse compact format and summarize
40+
lines = result.stdout.strip().split("\n")
41+
error_count = sum(
42+
1 for line in lines if "Error" in line or "error" in line.lower()
43+
)
44+
warning_count = sum(
45+
1 for line in lines if "Warning" in line or "warning" in line.lower()
46+
)
47+
48+
if error_count > 0 or warning_count > 0:
49+
summary = []
50+
if error_count > 0:
51+
summary.append(f"{error_count} error(s)")
52+
if warning_count > 0:
53+
summary.append(f"{warning_count} warning(s)")
54+
55+
# Include first few actual errors
56+
sample_errors = [
57+
line
58+
for line in lines[:5]
59+
if "Error" in line or "error" in line.lower()
60+
]
61+
62+
return f"ESLint: {', '.join(summary)}. " + " | ".join(sample_errors[:3])
63+
except (subprocess.TimeoutExpired, FileNotFoundError):
64+
pass
65+
66+
return None
67+
68+
69+
def extract_files_from_tool_input(tool_name: str, tool_input: dict) -> list[str]:
70+
"""Extract file paths from tool input based on tool type."""
71+
files = []
72+
73+
if tool_name in {"editFiles", "edit_files"}:
74+
# editFiles has a 'files' array
75+
if isinstance(tool_input, dict):
76+
files.extend(tool_input.get("files", []))
77+
elif tool_name in {"createFile", "create_file"}:
78+
# createFile has 'filePath'
79+
if isinstance(tool_input, dict) and "filePath" in tool_input:
80+
files.append(tool_input["filePath"])
81+
elif tool_name == "replace_string_in_file":
82+
# replace_string_in_file has 'filePath'
83+
if isinstance(tool_input, dict) and "filePath" in tool_input:
84+
files.append(tool_input["filePath"])
85+
86+
return files
87+
88+
89+
def main() -> int:
90+
"""Main entry point."""
91+
# Read input from stdin
92+
try:
93+
input_data = json.load(sys.stdin)
94+
except json.JSONDecodeError:
95+
input_data = {}
96+
97+
tool_name = input_data.get("tool_name", "")
98+
tool_input = input_data.get("tool_input", {})
99+
repo_root = Path(input_data.get("cwd", os.getcwd()))
100+
101+
# Only process file edit tools
102+
if tool_name not in FILE_EDIT_TOOLS:
103+
print(json.dumps({}))
104+
return 0
105+
106+
# Extract files that were edited
107+
files = extract_files_from_tool_input(tool_name, tool_input)
108+
if not files:
109+
print(json.dumps({}))
110+
return 0
111+
112+
# Run ESLint on TypeScript files
113+
lint_result = run_eslint(files, repo_root)
114+
115+
# Build response
116+
if lint_result:
117+
response = {
118+
"hookSpecificOutput": {
119+
"hookEventName": "PostToolUse",
120+
"additionalContext": lint_result,
121+
}
122+
}
123+
else:
124+
response = {}
125+
126+
print(json.dumps(response))
127+
return 0
128+
129+
130+
if __name__ == "__main__":
131+
sys.exit(main())

0 commit comments

Comments
 (0)