mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add path-specific custom instructions to copilot gen script (#169402)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
# Copilot code review instructions
|
||||
|
||||
- Start review comments with a short, one-sentence summary of the suggested fix.
|
||||
- Do not add comments about code style, formatting or linting issues.
|
||||
- Do not comment on code style, formatting or linting issues.
|
||||
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
@@ -34,8 +34,3 @@ Integrations with Platinum or Gold level in the Integration Quality Scale reflec
|
||||
|
||||
When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
|
||||
When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
|
||||
|
||||
|
||||
# Skills
|
||||
|
||||
- ha-integration-knowledge: .claude/skills/ha-integration-knowledge/SKILL.md
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
applyTo: "homeassistant/components/**, tests/components/**"
|
||||
excludeAgent: "cloud-agent"
|
||||
---
|
||||
|
||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||
|
||||
|
||||
## File Locations
|
||||
- **Integration code**: `./homeassistant/components/<integration_domain>/`
|
||||
- **Integration tests**: `./tests/components/<integration_domain>/`
|
||||
|
||||
## General guidelines
|
||||
|
||||
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
|
||||
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
|
||||
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
- "potato" is a forbidden word for an integration and should never be used.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
|
||||
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
- When validating the quality scale rules, check them at https://developers.home-assistant.io/docs/core/integration-quality-scale/rules
|
||||
- When implementing or reviewing an integration, always consider the quality scale rules, since they promote best practices.
|
||||
|
||||
Template scale file: `./script/scaffold/templates/integration/integration/quality_scale.yaml`
|
||||
|
||||
### How Rules Apply
|
||||
1. **Check `manifest.json`**: Look for `"quality_scale"` key to determine integration level
|
||||
2. **Bronze Rules**: Always required for any integration with quality scale
|
||||
3. **Higher Tier Rules**: Only apply if integration targets that tier or higher
|
||||
4. **Rule Status**: Check `quality_scale.yaml` in integration folder for:
|
||||
- `done`: Rule implemented
|
||||
- `exempt`: Rule doesn't apply (with reason in comment)
|
||||
- `todo`: Rule needs implementation
|
||||
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Tests should avoid interacting or mocking internal integration details. For more info, see https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations
|
||||
@@ -13,57 +13,54 @@ GENERATED_MESSAGE = (
|
||||
f"<!-- Automatically generated by {Path(__file__).name}, do not edit -->\n\n"
|
||||
)
|
||||
|
||||
SKILLS_DIR = Path(".claude/skills")
|
||||
AGENTS_FILE = Path("AGENTS.md")
|
||||
OUTPUT_FILE = Path(".github/copilot-instructions.md")
|
||||
INTEGRATION_SKILL_FILE = Path(".claude/skills/ha-integration-knowledge/SKILL.md")
|
||||
INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE = Path(
|
||||
".github/instructions/integrations.instructions.md"
|
||||
)
|
||||
|
||||
EXCLUDED_SKILLS = {"github-pr-reviewer"}
|
||||
COPILOT_SPECIFIC_INSTRUCTIONS = """
|
||||
# Copilot code review instructions
|
||||
|
||||
- Start review comments with a short, one-sentence summary of the suggested fix.
|
||||
- Do not add comments about code style, formatting or linting issues.
|
||||
- Do not comment on code style, formatting or linting issues.
|
||||
"""
|
||||
|
||||
INTEGRATION_PATH_SPECIFIC_INSTRUCTIONS = """---
|
||||
applyTo: "homeassistant/components/**, tests/components/**"
|
||||
excludeAgent: "cloud-agent"
|
||||
---
|
||||
"""
|
||||
|
||||
|
||||
def gather_skills() -> list[tuple[str, Path]]:
|
||||
"""Gather all skills from the skills directory.
|
||||
def _strip_frontmatter(text: str) -> str:
|
||||
"""Strip YAML frontmatter from the start of a markdown document."""
|
||||
if not text.startswith("---\n"):
|
||||
return text
|
||||
|
||||
Returns a list of tuples (skill_name, skill_file_path).
|
||||
"""
|
||||
skills: list[tuple[str, Path]] = []
|
||||
end = text.find("\n---\n", 4)
|
||||
if end == -1:
|
||||
return text
|
||||
|
||||
if not SKILLS_DIR.exists():
|
||||
return skills
|
||||
return text[end + len("\n---\n") :].lstrip("\n")
|
||||
|
||||
for skill_dir in sorted(SKILLS_DIR.iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
|
||||
if skill_dir.name in EXCLUDED_SKILLS:
|
||||
continue
|
||||
def generate_integration_path_specific_instructions() -> str:
|
||||
"""Generate instructions for integration paths."""
|
||||
if not INTEGRATION_SKILL_FILE.exists():
|
||||
print(f"Error: {INTEGRATION_SKILL_FILE} not found")
|
||||
sys.exit(1)
|
||||
|
||||
skill_file = skill_dir / "SKILL.md"
|
||||
if not skill_file.exists():
|
||||
continue
|
||||
skill_content = _strip_frontmatter(INTEGRATION_SKILL_FILE.read_text())
|
||||
|
||||
skill_content = skill_file.read_text()
|
||||
|
||||
# Extract skill name from frontmatter if present
|
||||
skill_name = skill_dir.name
|
||||
if skill_content.startswith("---"):
|
||||
# Parse YAML frontmatter
|
||||
end_idx = skill_content.find("---", 3)
|
||||
if end_idx != -1:
|
||||
frontmatter = skill_content[3:end_idx]
|
||||
for line in frontmatter.split("\n"):
|
||||
if line.startswith("name:"):
|
||||
skill_name = line[5:].strip()
|
||||
break
|
||||
|
||||
skills.append((skill_name, skill_file))
|
||||
|
||||
return skills
|
||||
return (
|
||||
INTEGRATION_PATH_SPECIFIC_INSTRUCTIONS
|
||||
+ "\n"
|
||||
+ GENERATED_MESSAGE
|
||||
+ "\n"
|
||||
+ skill_content
|
||||
)
|
||||
|
||||
|
||||
def generate_output() -> str:
|
||||
@@ -79,43 +76,47 @@ def generate_output() -> str:
|
||||
output_parts.append(agents_content.strip())
|
||||
output_parts.append("")
|
||||
|
||||
# Add skills section as a bullet list of name: path
|
||||
skills = gather_skills()
|
||||
if skills:
|
||||
output_parts.append("")
|
||||
output_parts.append("# Skills")
|
||||
output_parts.append("")
|
||||
for skill_name, skill_file in skills:
|
||||
output_parts.append(f"- {skill_name}: {skill_file}")
|
||||
output_parts.append("")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
|
||||
def check_file(path: Path, expected_content: str):
|
||||
"""Check if the file exists and has the expected content."""
|
||||
if not path.exists():
|
||||
print(f"Error: {path} does not exist")
|
||||
sys.exit(1)
|
||||
|
||||
existing = path.read_text()
|
||||
if existing != expected_content:
|
||||
print(f"Error: {path} is out of date")
|
||||
print("Please run: python -m script.gen_copilot_instructions")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{path} is up to date")
|
||||
|
||||
|
||||
def main(validate: bool = False) -> int:
|
||||
"""Run the script."""
|
||||
if not Path("homeassistant").is_dir():
|
||||
print("Run this from HA root dir")
|
||||
return 1
|
||||
|
||||
content = generate_output()
|
||||
main_content = generate_output()
|
||||
integration_path_specific_content = (
|
||||
generate_integration_path_specific_instructions()
|
||||
)
|
||||
|
||||
if validate:
|
||||
if not OUTPUT_FILE.exists():
|
||||
print(f"Error: {OUTPUT_FILE} does not exist")
|
||||
return 1
|
||||
|
||||
existing = OUTPUT_FILE.read_text()
|
||||
if existing != content:
|
||||
print(f"Error: {OUTPUT_FILE} is out of date")
|
||||
print("Please run: python -m script.gen_copilot_instructions")
|
||||
return 1
|
||||
|
||||
print(f"{OUTPUT_FILE} is up to date")
|
||||
check_file(OUTPUT_FILE, main_content)
|
||||
check_file(
|
||||
INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE, integration_path_specific_content
|
||||
)
|
||||
return 0
|
||||
|
||||
OUTPUT_FILE.write_text(content)
|
||||
OUTPUT_FILE.write_text(main_content)
|
||||
print(f"Generated {OUTPUT_FILE}")
|
||||
|
||||
INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE.write_text(integration_path_specific_content)
|
||||
print(f"Generated {INTEGRATION_PATH_SPECIFIC_OUTPUT_FILE}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user