Skip to content

Commit 1f45f37

Browse files
committed
add: portability CI gate, refactor Hermes-specific reference in safe-editing
what: - .github/scripts/check-portability.py — portability gate scanning shipped SKILL.md and reference files for agent-specific references. Includes inline exemption mechanism (# portability: allow-platform-ref) for documentation-only platform path mentions. - .github/workflows/ci.yml — added .github/scripts/** to trigger paths and portability check as a CI step - references/safe-editing.md — replaced Hermes-specific import (from hermes_tools import terminal, write_file) with portable Python stdlib open/read/write; updated section title and lead-in text - references/project-orientation.md — marked .cursor/rules/* mention as documentation-only with exemption comment why: - safe-editing.md had a Hermes-specific Python import in the shipped reference surface that would break non-Hermes agents - No portability gate existed to prevent future drift - project-orientation.md's agent file listing included .cursor/ as a documentation reference, not an instruction — needed exemption
1 parent 55af1a9 commit 1f45f37

4 files changed

Lines changed: 82 additions & 9 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
"""Check for agent-specific references in portable skill files.
3+
4+
Ensures python-project-workflow's runtime surface remains cross-agent
5+
compatible. Scans both SKILL.md and references/*.md under skills/.
6+
7+
"""
8+
from __future__ import annotations
9+
10+
import re
11+
import sys
12+
from pathlib import Path
13+
14+
SKILLS_DIR = Path("skills")
15+
16+
FORBIDDEN_PATTERNS = (
17+
("Hermes tool name", re.compile(r"\bskill_(?:view|manage)\b", re.IGNORECASE)),
18+
("Hermes config path", re.compile(r"~/\.hermes(?:/|\b)", re.IGNORECASE)),
19+
(
20+
"Hermes CLI command",
21+
re.compile(
22+
r"\bhermes\s+(?:skills?|config|tools?|setup|help|doctor|gateway|run|serve|cron)\b",
23+
re.IGNORECASE,
24+
),
25+
),
26+
("Hermes Python import", re.compile(r"from hermes_tools\b", re.IGNORECASE)),
27+
("Claude Code agent reference", re.compile(r"\bClaude\(\)", re.IGNORECASE)),
28+
("Gemini CLI command", re.compile(r"\bgemini\s+skills?\b", re.IGNORECASE)),
29+
(
30+
"Platform-specific path",
31+
re.compile(r"\.(?:claude|cursor|codex|opencode|gemini)/"),
32+
),
33+
)
34+
35+
36+
def scan_file(path: Path) -> list[tuple[Path, int, str, str]]:
37+
violations: list[tuple[Path, int, str, str]] = []
38+
text = path.read_text(encoding="utf-8")
39+
for line_no, line in enumerate(text.splitlines(), start=1):
40+
# Exemption: lines with this comment are documentation references, not instructions
41+
if "# portability: allow-platform-ref" in line:
42+
continue
43+
for label, pattern in FORBIDDEN_PATTERNS:
44+
if pattern.search(line):
45+
violations.append((path, line_no, label, line.rstrip()))
46+
return violations
47+
48+
49+
def main() -> int:
50+
violations: list[tuple[Path, int, str, str]] = []
51+
for path in sorted(SKILLS_DIR.rglob("*.md")):
52+
try:
53+
violations.extend(scan_file(path))
54+
except OSError as exc:
55+
print(f"FAIL: could not read {path}: {exc}", file=sys.stderr)
56+
return 1
57+
58+
if violations:
59+
print("FAIL: agent-specific references found in skills/ — will break non-Hermes agents:")
60+
for path, line_no, label, line in violations:
61+
print(f" {path}:{line_no}: {label}: {line}")
62+
return 1
63+
64+
print("PASS: no agent-specific references in skills/")
65+
return 0
66+
67+
68+
if __name__ == "__main__":
69+
sys.exit(main())

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
paths:
77
- ".github/workflows/ci.yml"
8+
- ".github/scripts/**"
89
- "README.md"
910
- "CITATION.cff"
1011
- "scripts/**"
@@ -13,6 +14,7 @@ on:
1314
branches: [main]
1415
paths:
1516
- ".github/workflows/ci.yml"
17+
- ".github/scripts/**"
1618
- "README.md"
1719
- "CITATION.cff"
1820
- "scripts/**"
@@ -40,6 +42,9 @@ jobs:
4042
python -m pip install --upgrade pip
4143
python -m pip install ruff
4244
45+
- name: Check cross-agent portability
46+
run: python3 .github/scripts/check-portability.py
47+
4348
- name: Run skill source checks
4449
run: python3 scripts/validate.py
4550

skills/python-project-workflow/references/project-orientation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Before giving advice, inspect the repository to understand its current state and
5252

5353
### Agent and Developer Documentation
5454

55-
- `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/*`, `.continue/rules/*`, `.github/copilot-instructions.md`, `CONVENTIONS.md`,
55+
- `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/*` # portability: allow-platform-ref, `.continue/rules/*`, `.github/copilot-instructions.md`, `CONVENTIONS.md`,
5656
`.aider*`
5757
- `README.md`, `CONTRIBUTING.md`, `DEVELOPMENT.md`
5858
- Documentation directories: `docs/`, `doc/`, `documentation/`

skills/python-project-workflow/references/safe-editing.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,24 @@ Editing files with dense backslash patterns (e.g. sed BRE capture groups `\(...\
1212

1313
## Safe Workflow (Ranked by Reliability)
1414

15-
### 1. First Resort: `write_file` with `terminal("cat")` input
15+
### 1. First Resort: Python stdlib read/write
1616

17-
Use `execute_code` with `terminal("cat")` to read raw bytes, then `write_file` — zero escaping layers:
17+
Use Python stdlib — open the file, read, replace, write — zero escaping layers:
1818

1919
```python
20-
from hermes_tools import terminal, write_file
21-
2220
# Read raw file (no escaping, no format wrapping)
23-
r = terminal(["cat", "/path/to/file"])
24-
content = r["output"]
21+
with open("/path/to/file") as f:
22+
content = f.read()
2523

2624
# Python string replacement — backslashes in your replacement are literal
2725
content = content.replace(
2826
'old_backslash_pattern', # literal bytes from the file
2927
'new_backslash_pattern' # literal bytes you want
3028
)
3129

32-
# Write_file accepts raw content with no escaping
33-
write_file("/path/to/file", content)
30+
# Write back
31+
with open("/path/to/file", "w") as f:
32+
f.write(content)
3433
```
3534

3635
Use raw strings `r'...'` so Python doesn't process the backslashes. Verify the result with `od -A x -t x1z | grep '5c'` (see Diagnostic below).

0 commit comments

Comments
 (0)