mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-09 13:39:26 +00:00
fix(skills): read skill markdown as UTF-8 (#4995)
* fix(skills): read skill markdown as UTF-8 * fix(skills): complete UTF-8 handling in skill creator * fix(skills): finish UTF-8 skill-creator I/O --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
06e54008c9
commit
a5ec7f2831
@ -228,6 +228,9 @@ These apply repo-wide; module guides own the module-specific detail.
|
||||
frontend tests live in `frontend/tests/`.
|
||||
- **Format before pushing** — run `make format` (backend) / `pnpm check` (frontend). Backend
|
||||
CI enforces `ruff format --check`, so formatting must be clean before a push.
|
||||
- **Skill text encoding** — treat `SKILL.md` and other textual skill resources as UTF-8;
|
||||
Python utilities that read or write them must pass `encoding="utf-8"` rather than
|
||||
relying on the platform locale.
|
||||
- **Version sources must stay in lockstep** — a release version must match identically in
|
||||
`backend/pyproject.toml`, `frontend/package.json`, and `deploy/helm/deer-flow/Chart.yaml`
|
||||
(`version` + `appVersion`). Pushing a `v*` git tag triggers CI that runs
|
||||
|
||||
@ -877,6 +877,8 @@ Skills are loaded progressively — only when the task needs them, not all at on
|
||||
|
||||
A skill directory is a package boundary: once DeerFlow finds its `SKILL.md`, nested `SKILL.md` files under that package (for example evaluation fixtures) remain supporting data and are not registered as runtime skills. Namespace directories without their own `SKILL.md` can still group nested skills.
|
||||
|
||||
Skill Markdown and bundled text resources use UTF-8. Skill-creator CLI and review utilities read and write text explicitly as UTF-8 so localized skills behave consistently across operating systems.
|
||||
|
||||
Users can explicitly activate an enabled skill for a single turn by starting the request with `/skill-name`, for example `/data-analysis analyze uploads/foo.csv`. DeerFlow loads that skill's `SKILL.md` as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as `/new` and `/help`.
|
||||
|
||||
An enabled skill's `allowed-tools` policy applies only after that skill is explicitly slash-activated or captured in the agent's active skill context after a `read_file` load. Merely enabling, advertising, or listing a skill in a custom agent or subagent `skills` allowlist does not reduce that agent's normal toolset; subagents use the same progressive discovery and activation policy as the lead agent. During a slash-activated run, that explicit skill's policy is authoritative: reading another `SKILL.md` may provide instructions but cannot widen the slash skill's tools. Without slash activation, policies from skills actually loaded into active context retain their union semantics. Once active, the policy filters both model-visible tool schemas and tool execution. Framework discovery tools (`tool_search` and `describe_skill`) remain available so an allowed deferred tool or installed skill can still be discovered, but discovery and promotion never grant permission to execute a business tool omitted from `allowed-tools`. `task` is not framework-exempt; a restrictive skill must list it explicitly to delegate to a subagent. Per-step policy decisions are internal runtime context and are removed from observable or persisted context copies. Registry failures and an active set with no remaining valid skill fail closed to framework-safe tools; individual stale paths are ignored only when another valid active skill remains. This is best-effort behavioral scoping, not a hard security boundary: loading skill instructions through another tool is not captured, and active-skill entries can be evicted from bounded context.
|
||||
|
||||
149
backend/tests/test_skill_creator_quick_validate.py
Normal file
149
backend/tests/test_skill_creator_quick_validate.py
Normal file
@ -0,0 +1,149 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SKILL_CREATOR_ROOT = REPO_ROOT / "skills" / "public" / "skill-creator"
|
||||
SCRIPTS_DIR = SKILL_CREATOR_ROOT / "scripts"
|
||||
VALIDATOR_PATH = SCRIPTS_DIR / "quick_validate.py"
|
||||
|
||||
|
||||
def _load_validator():
|
||||
spec = importlib.util.spec_from_file_location("deerflow_skill_creator_quick_validate", VALIDATOR_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _load_script(name: str):
|
||||
path = SCRIPTS_DIR / f"{name}.py"
|
||||
spec = importlib.util.spec_from_file_location(f"deerflow_skill_creator_{name}", path)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_validate_skill_reads_markdown_as_utf8(tmp_path: Path, monkeypatch) -> None:
|
||||
validator = _load_validator()
|
||||
skill_dir = tmp_path / "localized-skill"
|
||||
skill_dir.mkdir()
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text(
|
||||
"---\nname: localized-skill\ndescription: 处理中文内容\n---\n\n# 中文技能\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
original_read_text = Path.read_text
|
||||
|
||||
def require_explicit_encoding(self: Path, encoding: str | None = None, errors: str | None = None) -> str:
|
||||
if self == skill_md and encoding is None:
|
||||
raise UnicodeDecodeError("gbk", b"\x80", 0, 1, "illegal multibyte sequence")
|
||||
return original_read_text(self, encoding=encoding, errors=errors)
|
||||
|
||||
monkeypatch.setattr(Path, "read_text", require_explicit_encoding)
|
||||
|
||||
assert validator.validate_skill(skill_dir) == (True, "Skill is valid!")
|
||||
|
||||
|
||||
def test_validate_skill_reports_invalid_utf8_without_raising(tmp_path: Path) -> None:
|
||||
validator = _load_validator()
|
||||
skill_dir = tmp_path / "invalid-encoding"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_bytes(b"---\nname: invalid-encoding\ndescription: \xff\n---\n")
|
||||
|
||||
assert validator.validate_skill(skill_dir) == (False, "SKILL.md is not valid UTF-8")
|
||||
|
||||
|
||||
def test_package_skill_reports_invalid_utf8_without_traceback(tmp_path: Path, monkeypatch, capsys) -> None:
|
||||
monkeypatch.syspath_prepend(str(SKILL_CREATOR_ROOT))
|
||||
packager = _load_script("package_skill")
|
||||
skill_dir = tmp_path / "invalid-encoding"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_bytes(b"---\nname: invalid-encoding\ndescription: \xff\n---\n")
|
||||
output_dir = tmp_path / "dist"
|
||||
|
||||
assert packager.package_skill(skill_dir, output_dir) is None
|
||||
assert "Validation failed: SKILL.md is not valid UTF-8" in capsys.readouterr().out
|
||||
assert not list(tmp_path.rglob("*.skill"))
|
||||
|
||||
|
||||
def test_shared_skill_parser_reads_markdown_as_utf8(tmp_path: Path, monkeypatch) -> None:
|
||||
utils = _load_script("utils")
|
||||
skill_dir = tmp_path / "localized-skill"
|
||||
skill_dir.mkdir()
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text(
|
||||
"---\nname: localized-skill\ndescription: 处理中文内容\n---\n\n# 中文技能\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
original_read_text = Path.read_text
|
||||
|
||||
def require_explicit_encoding(self: Path, encoding: str | None = None, errors: str | None = None) -> str:
|
||||
if self == skill_md and encoding is None:
|
||||
raise UnicodeDecodeError("gbk", b"\x80", 0, 1, "illegal multibyte sequence")
|
||||
return original_read_text(self, encoding=encoding, errors=errors)
|
||||
|
||||
monkeypatch.setattr(Path, "read_text", require_explicit_encoding)
|
||||
|
||||
assert utils.parse_skill_md(skill_dir) == (
|
||||
"localized-skill",
|
||||
"处理中文内容",
|
||||
skill_md.read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
|
||||
def test_improve_description_uses_utf8_for_claude_text_io(monkeypatch) -> None:
|
||||
monkeypatch.syspath_prepend(str(SKILL_CREATOR_ROOT))
|
||||
improve_description = _load_script("improve_description")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return SimpleNamespace(returncode=0, stdout="改进后的描述", stderr="")
|
||||
|
||||
monkeypatch.setattr(improve_description.subprocess, "run", fake_run)
|
||||
|
||||
assert improve_description._call_claude("处理中文内容🙂", None) == "改进后的描述"
|
||||
assert captured["input"] == "处理中文内容🙂"
|
||||
assert captured["encoding"] == "utf-8"
|
||||
|
||||
|
||||
def test_skill_creator_text_io_declares_utf8() -> None:
|
||||
missing_encoding: list[str] = []
|
||||
|
||||
for script_path in sorted(SKILL_CREATOR_ROOT.rglob("*.py")):
|
||||
tree = ast.parse(script_path.read_text(encoding="utf-8"), filename=str(script_path))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
|
||||
if isinstance(node.func, ast.Attribute) and node.func.attr in {"read_text", "write_text"}:
|
||||
operation = node.func.attr
|
||||
elif isinstance(node.func, ast.Name) and node.func.id == "open":
|
||||
mode = node.args[1] if len(node.args) > 1 else next((keyword.value for keyword in node.keywords if keyword.arg == "mode"), None)
|
||||
if isinstance(mode, ast.Constant) and isinstance(mode.value, str) and "b" in mode.value:
|
||||
continue
|
||||
operation = "open"
|
||||
elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "subprocess":
|
||||
keywords = {keyword.arg: keyword.value for keyword in node.keywords if keyword.arg is not None}
|
||||
text_mode = any(isinstance(keywords.get(name), ast.Constant) and keywords[name].value is True for name in ("text", "universal_newlines"))
|
||||
if not text_mode and "encoding" not in keywords:
|
||||
continue
|
||||
operation = f"subprocess.{node.func.attr}"
|
||||
else:
|
||||
continue
|
||||
|
||||
encoding = next((keyword.value for keyword in node.keywords if keyword.arg == "encoding"), None)
|
||||
if not isinstance(encoding, ast.Constant) or encoding.value != "utf-8":
|
||||
relative_path = script_path.relative_to(SKILL_CREATOR_ROOT)
|
||||
missing_encoding.append(f"{relative_path}:{node.lineno} {operation}")
|
||||
|
||||
assert not missing_encoding, f"text I/O must declare encoding='utf-8': {missing_encoding}"
|
||||
@ -87,7 +87,7 @@ def build_run(root: Path, run_dir: Path) -> dict | None:
|
||||
for candidate in [run_dir / "eval_metadata.json", run_dir.parent / "eval_metadata.json"]:
|
||||
if candidate.exists():
|
||||
try:
|
||||
metadata = json.loads(candidate.read_text())
|
||||
metadata = json.loads(candidate.read_text(encoding="utf-8"))
|
||||
prompt = metadata.get("prompt", "")
|
||||
eval_id = metadata.get("eval_id")
|
||||
except (json.JSONDecodeError, OSError):
|
||||
@ -100,7 +100,7 @@ def build_run(root: Path, run_dir: Path) -> dict | None:
|
||||
for candidate in [run_dir / "transcript.md", run_dir / "outputs" / "transcript.md"]:
|
||||
if candidate.exists():
|
||||
try:
|
||||
text = candidate.read_text()
|
||||
text = candidate.read_text(encoding="utf-8")
|
||||
match = re.search(r"## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)", text)
|
||||
if match:
|
||||
prompt = match.group(1).strip()
|
||||
@ -127,7 +127,7 @@ def build_run(root: Path, run_dir: Path) -> dict | None:
|
||||
for candidate in [run_dir / "grading.json", run_dir.parent / "grading.json"]:
|
||||
if candidate.exists():
|
||||
try:
|
||||
grading = json.loads(candidate.read_text())
|
||||
grading = json.loads(candidate.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
if grading:
|
||||
@ -149,7 +149,7 @@ def embed_file(path: Path) -> dict:
|
||||
|
||||
if ext in TEXT_EXTENSIONS:
|
||||
try:
|
||||
content = path.read_text(errors="replace")
|
||||
content = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
content = "(Error reading file)"
|
||||
return {
|
||||
@ -218,7 +218,7 @@ def load_previous_iteration(workspace: Path) -> dict[str, dict]:
|
||||
feedback_path = workspace / "feedback.json"
|
||||
if feedback_path.exists():
|
||||
try:
|
||||
data = json.loads(feedback_path.read_text())
|
||||
data = json.loads(feedback_path.read_text(encoding="utf-8"))
|
||||
feedback_map = {
|
||||
r["run_id"]: r["feedback"]
|
||||
for r in data.get("reviews", [])
|
||||
@ -251,7 +251,7 @@ def generate_html(
|
||||
) -> str:
|
||||
"""Generate the complete standalone HTML page with embedded data."""
|
||||
template_path = Path(__file__).parent / "viewer.html"
|
||||
template = template_path.read_text()
|
||||
template = template_path.read_text(encoding="utf-8")
|
||||
|
||||
# Build previous_feedback and previous_outputs maps for the template
|
||||
previous_feedback: dict[str, str] = {}
|
||||
@ -313,7 +313,7 @@ class ReviewHandler(BaseHTTPRequestHandler):
|
||||
benchmark = None
|
||||
if self.benchmark_path and self.benchmark_path.exists():
|
||||
try:
|
||||
benchmark = json.loads(self.benchmark_path.read_text())
|
||||
benchmark = json.loads(self.benchmark_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
html = generate_html(runs, self.skill_name, self.previous, benchmark)
|
||||
@ -343,7 +343,7 @@ class ReviewHandler(BaseHTTPRequestHandler):
|
||||
data = json.loads(body)
|
||||
if not isinstance(data, dict) or "reviews" not in data:
|
||||
raise ValueError("Expected JSON object with 'reviews' key")
|
||||
self.feedback_path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
self.feedback_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
resp = b'{"ok":true}'
|
||||
self.send_response(200)
|
||||
except (json.JSONDecodeError, OSError, ValueError) as e:
|
||||
@ -401,14 +401,14 @@ def main() -> None:
|
||||
benchmark = None
|
||||
if benchmark_path and benchmark_path.exists():
|
||||
try:
|
||||
benchmark = json.loads(benchmark_path.read_text())
|
||||
benchmark = json.loads(benchmark_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
if args.static:
|
||||
html = generate_html(runs, skill_name, previous, benchmark)
|
||||
args.static.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.static.write_text(html)
|
||||
args.static.write_text(html, encoding="utf-8")
|
||||
print(f"\n Static viewer written to: {args.static}\n")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@ -311,12 +311,12 @@ def main():
|
||||
if args.input == "-":
|
||||
data = json.load(sys.stdin)
|
||||
else:
|
||||
data = json.loads(Path(args.input).read_text())
|
||||
data = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
||||
|
||||
html_output = generate_html(data, skill_name=args.skill_name)
|
||||
|
||||
if args.output:
|
||||
Path(args.output).write_text(html_output)
|
||||
Path(args.output).write_text(html_output, encoding="utf-8")
|
||||
print(f"Report written to {args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(html_output)
|
||||
|
||||
@ -36,7 +36,7 @@ def _call_claude(prompt: str, model: str | None, timeout: int = 300) -> str:
|
||||
cmd,
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
timeout=timeout,
|
||||
)
|
||||
@ -186,7 +186,7 @@ Please respond with only the new description text in <new_description> tags, not
|
||||
if log_dir:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / f"improve_iter_{iteration or 'unknown'}.json"
|
||||
log_file.write_text(json.dumps(transcript, indent=2))
|
||||
log_file.write_text(json.dumps(transcript, indent=2), encoding="utf-8")
|
||||
|
||||
return description
|
||||
|
||||
@ -205,10 +205,10 @@ def main():
|
||||
print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
eval_results = json.loads(Path(args.eval_results).read_text())
|
||||
eval_results = json.loads(Path(args.eval_results).read_text(encoding="utf-8"))
|
||||
history = []
|
||||
if args.history:
|
||||
history = json.loads(Path(args.history).read_text())
|
||||
history = json.loads(Path(args.history).read_text(encoding="utf-8"))
|
||||
|
||||
name, _, content = parse_skill_md(skill_path)
|
||||
current_description = eval_results["description"]
|
||||
|
||||
@ -227,7 +227,7 @@ def init_skill(skill_name, path):
|
||||
|
||||
skill_md_path = skill_dir / 'SKILL.md'
|
||||
try:
|
||||
skill_md_path.write_text(skill_content)
|
||||
skill_md_path.write_text(skill_content, encoding="utf-8")
|
||||
print("✅ Created SKILL.md")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating SKILL.md: {e}")
|
||||
@ -239,7 +239,7 @@ def init_skill(skill_name, path):
|
||||
scripts_dir = skill_dir / 'scripts'
|
||||
scripts_dir.mkdir(exist_ok=True)
|
||||
example_script = scripts_dir / 'example.py'
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name), encoding="utf-8")
|
||||
example_script.chmod(0o755)
|
||||
print("✅ Created scripts/example.py")
|
||||
|
||||
@ -247,14 +247,14 @@ def init_skill(skill_name, path):
|
||||
references_dir = skill_dir / 'references'
|
||||
references_dir.mkdir(exist_ok=True)
|
||||
example_reference = references_dir / 'api_reference.md'
|
||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title), encoding="utf-8")
|
||||
print("✅ Created references/api_reference.md")
|
||||
|
||||
# Create assets/ directory with example asset placeholder
|
||||
assets_dir = skill_dir / 'assets'
|
||||
assets_dir.mkdir(exist_ok=True)
|
||||
example_asset = assets_dir / 'example_asset.txt'
|
||||
example_asset.write_text(EXAMPLE_ASSET)
|
||||
example_asset.write_text(EXAMPLE_ASSET, encoding="utf-8")
|
||||
print("✅ Created assets/example_asset.txt")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating resource directories: {e}")
|
||||
|
||||
@ -18,7 +18,10 @@ def validate_skill(skill_path):
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
# Read and validate frontmatter
|
||||
content = skill_md.read_text()
|
||||
try:
|
||||
content = skill_md.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return False, "SKILL.md is not valid UTF-8"
|
||||
if not content.startswith('---'):
|
||||
return False, "No YAML frontmatter found"
|
||||
|
||||
@ -99,4 +102,4 @@ if __name__ == "__main__":
|
||||
|
||||
valid, message = validate_skill(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(0 if valid else 1)
|
||||
sys.exit(0 if valid else 1)
|
||||
|
||||
@ -65,7 +65,7 @@ def run_single_query(
|
||||
f"# {skill_name}\n\n"
|
||||
f"This skill handles: {skill_description}\n"
|
||||
)
|
||||
command_file.write_text(command_content)
|
||||
command_file.write_text(command_content, encoding="utf-8")
|
||||
|
||||
cmd = [
|
||||
"claude",
|
||||
@ -269,7 +269,7 @@ def main():
|
||||
parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
|
||||
args = parser.parse_args()
|
||||
|
||||
eval_set = json.loads(Path(args.eval_set).read_text())
|
||||
eval_set = json.loads(Path(args.eval_set).read_text(encoding="utf-8"))
|
||||
skill_path = Path(args.skill_path)
|
||||
|
||||
if not (skill_path / "SKILL.md").exists():
|
||||
|
||||
@ -148,7 +148,7 @@ def run_loop(
|
||||
"test_size": len(test_set),
|
||||
"history": history,
|
||||
}
|
||||
live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name))
|
||||
live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name), encoding="utf-8")
|
||||
|
||||
if verbose:
|
||||
def print_eval_stats(label, results, elapsed):
|
||||
@ -258,7 +258,7 @@ def main():
|
||||
parser.add_argument("--results-dir", default=None, help="Save all outputs (results.json, report.html, log.txt) to a timestamped subdirectory here")
|
||||
args = parser.parse_args()
|
||||
|
||||
eval_set = json.loads(Path(args.eval_set).read_text())
|
||||
eval_set = json.loads(Path(args.eval_set).read_text(encoding="utf-8"))
|
||||
skill_path = Path(args.skill_path)
|
||||
|
||||
if not (skill_path / "SKILL.md").exists():
|
||||
@ -275,7 +275,10 @@ def main():
|
||||
else:
|
||||
live_report_path = Path(args.report)
|
||||
# Open the report immediately so the user can watch
|
||||
live_report_path.write_text("<html><body><h1>Starting optimization loop...</h1><meta http-equiv='refresh' content='5'></body></html>")
|
||||
live_report_path.write_text(
|
||||
"<html><body><h1>Starting optimization loop...</h1><meta http-equiv='refresh' content='5'></body></html>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
webbrowser.open(str(live_report_path))
|
||||
else:
|
||||
live_report_path = None
|
||||
@ -310,15 +313,15 @@ def main():
|
||||
json_output = json.dumps(output, indent=2)
|
||||
print(json_output)
|
||||
if results_dir:
|
||||
(results_dir / "results.json").write_text(json_output)
|
||||
(results_dir / "results.json").write_text(json_output, encoding="utf-8")
|
||||
|
||||
# Write final HTML report (without auto-refresh)
|
||||
if live_report_path:
|
||||
live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name))
|
||||
live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name), encoding="utf-8")
|
||||
print(f"\nReport: {live_report_path}", file=sys.stderr)
|
||||
|
||||
if results_dir and live_report_path:
|
||||
(results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name))
|
||||
(results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name), encoding="utf-8")
|
||||
|
||||
if results_dir:
|
||||
print(f"Results saved to: {results_dir}", file=sys.stderr)
|
||||
|
||||
@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
|
||||
"""Parse a SKILL.md file, returning (name, description, full_content)."""
|
||||
content = (skill_path / "SKILL.md").read_text()
|
||||
content = (skill_path / "SKILL.md").read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
if lines[0].strip() != "---":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user