Add path-specific custom instructions to copilot gen script (#169402)

This commit is contained in:
Abílio Costa
2026-04-29 08:19:10 +02:00
committed by GitHub
parent 44e94a82f1
commit 84d5085f3b
3 changed files with 107 additions and 64 deletions
+1 -6
View File
@@ -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
+59 -58
View File
@@ -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