feat: add unified 1.0.0 skill collection
This commit is contained in:
commit
cc4c380583
87 files changed
+6963
No files matched your search
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "generate-agents-md",
|
||||
"version": "1.0.0",
|
||||
"description": "创建、更新或审查 AGENTS.md;基于仓库事实生成精炼、准确的 Agent 指南。",
|
||||
"author": {
|
||||
"name": "EAPIL",
|
||||
"url": "https://git.playones.com/arechen/EapilSkillMarket"
|
||||
},
|
||||
"homepage": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"repository": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"license": "Proprietary",
|
||||
"keywords": [
|
||||
"eapil",
|
||||
"codex-skill",
|
||||
"generate-agents-md"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "AGENTS.md生成器",
|
||||
"shortDescription": "基于仓库事实创建、更新或审查精炼准确的 AGENTS.md。",
|
||||
"longDescription": "为 Codex 创建或维护 AGENTS.md,保留项目特有约束、明确命令工作目录、按需引用资料,并提供机械校验。其他 Agent 的兼容规则按需核对。",
|
||||
"developerName": "EAPIL",
|
||||
"category": "Developer Tools",
|
||||
"capabilities": [
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"defaultPrompt": [
|
||||
"使用 AGENTS.md生成器 帮我完成这个任务。"
|
||||
],
|
||||
"websiteURL": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"privacyPolicyURL": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"termsOfServiceURL": "https://git.playones.com/arechen/EapilSkillMarket",
|
||||
"brandColor": "#2563EB",
|
||||
"screenshots": [],
|
||||
"composerIcon": "./assets/market-icon.svg",
|
||||
"logo": "./assets/market-icon.svg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="360" viewBox="0 0 32 32"><title>AGENTS.md生成器</title><g fill="none"><path fill="url(#SVGklx3zJXo)" d="M7.5 3A4.5 4.5 0 0 0 3 7.5v17A4.5 4.5 0 0 0 7.5 29h17a4.5 4.5 0 0 0 4.5-4.5v-17A4.5 4.5 0 0 0 24.5 3z"/><path fill="url(#SVGxO13MgXq)" d="M7.5 3A4.5 4.5 0 0 0 3 7.5v17A4.5 4.5 0 0 0 7.5 29h17a4.5 4.5 0 0 0 4.5-4.5v-17A4.5 4.5 0 0 0 24.5 3z"/><path fill="url(#SVGxluVFb5q)" d="M7.5 3A4.5 4.5 0 0 0 3 7.5v17A4.5 4.5 0 0 0 7.5 29h17a4.5 4.5 0 0 0 4.5-4.5v-17A4.5 4.5 0 0 0 24.5 3z"/><path fill="url(#SVGPD31pBoO)" d="M7.5 3A4.5 4.5 0 0 0 3 7.5v17A4.5 4.5 0 0 0 7.5 29h17a4.5 4.5 0 0 0 4.5-4.5v-17A4.5 4.5 0 0 0 24.5 3z"/><path fill="url(#SVGOvZEKcyA)" d="M7.5 3A4.5 4.5 0 0 0 3 7.5v17A4.5 4.5 0 0 0 7.5 29h17a4.5 4.5 0 0 0 4.5-4.5v-17A4.5 4.5 0 0 0 24.5 3z"/><path fill="url(#SVGdi1M5dPd)" d="M12 10.5a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0m0 5.5a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0m-1.5 7a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3M14 10.5a1 1 0 0 0 1 1h7a1 1 0 1 0 0-2h-7a1 1 0 0 0-1 1m1 4.5a1 1 0 1 0 0 2h7a1 1 0 1 0 0-2zm-1 6.5a1 1 0 0 0 1 1h7a1 1 0 1 0 0-2h-7a1 1 0 0 0-1 1"/><path fill="url(#SVG2J5EFbQO)" d="m31.74 13.656l-.919-.299a1.9 1.9 0 0 1-1.199-1.197l-.298-.918a.363.363 0 0 0-.684 0l-.299.918a1.89 1.89 0 0 1-1.18 1.197l-.919.299a.363.363 0 0 0 0 .684l.919.298a1.9 1.9 0 0 1 1.198 1.202l.299.918a.363.363 0 0 0 .684 0l.299-.918a1.89 1.89 0 0 1 1.198-1.197l.919-.299a.363.363 0 0 0 0-.684z"/><path fill="url(#SVGNaWMwbPh)" d="M21.775 8.837a3.5 3.5 0 0 0-1.647-1.168l-1.684-.546a.665.665 0 0 1 0-1.254l1.684-.547a3.47 3.47 0 0 0 2.15-2.154l.014-.042l.547-1.682a.665.665 0 0 1 1.255 0l.547 1.682a3.47 3.47 0 0 0 2.198 2.196l1.683.547l.034.008a.665.665 0 0 1 0 1.254l-1.684.547a3.47 3.47 0 0 0-2.197 2.196l-.548 1.682l-.016.042a.664.664 0 0 1-1.238-.042l-.548-1.682a3.5 3.5 0 0 0-.55-1.037"/><defs><radialGradient id="SVGxO13MgXq" cx="0" cy="0" r="1" gradientTransform="matrix(-3.5 0 0 -1.74793 29 14.5)" gradientUnits="userSpaceOnUse"><stop stop-color="#4a43cb"/><stop offset="1" stop-color="#4a43cb" stop-opacity="0"/></radialGradient><radialGradient id="SVGxluVFb5q" cx="0" cy="0" r="1" gradientTransform="matrix(-7 0 0 -3.49587 23.5 7.5)" gradientUnits="userSpaceOnUse"><stop stop-color="#4a43cb"/><stop offset="1" stop-color="#4a43cb" stop-opacity="0"/></radialGradient><radialGradient id="SVGPD31pBoO" cx="0" cy="0" r="1" gradientTransform="matrix(0 6 -2.99646 0 23.5 7.5)" gradientUnits="userSpaceOnUse"><stop stop-color="#4a43cb"/><stop offset="1" stop-color="#4a43cb" stop-opacity="0"/></radialGradient><radialGradient id="SVGOvZEKcyA" cx="0" cy="0" r="1" gradientTransform="matrix(0 4 -1.99764 0 29 14.5)" gradientUnits="userSpaceOnUse"><stop stop-color="#4a43cb"/><stop offset="1" stop-color="#4a43cb" stop-opacity="0"/></radialGradient><radialGradient id="SVG2J5EFbQO" cx="0" cy="0" r="1" gradientTransform="rotate(63.379 17.2 6.887)scale(34.3364 26.737)" gradientUnits="userSpaceOnUse"><stop offset=".718" stop-color="#ffcd0f"/><stop offset=".991" stop-color="#e67505"/></radialGradient><radialGradient id="SVGNaWMwbPh" cx="0" cy="0" r="1" gradientTransform="rotate(61.2 24.745 -9.672)scale(47.2968 36.829)" gradientUnits="userSpaceOnUse"><stop offset=".698" stop-color="#ffcd0f"/><stop offset=".991" stop-color="#e67505"/></radialGradient><linearGradient id="SVGklx3zJXo" x1="3.929" x2="21.872" y1="7.875" y2="26.517" gradientUnits="userSpaceOnUse"><stop stop-color="#0fafff"/><stop offset="1" stop-color="#2764e7"/></linearGradient><linearGradient id="SVGdi1M5dPd" x1="11.692" x2="21.978" y1="10.077" y2="34.999" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#b3e0ff"/></linearGradient></defs></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: generate-agents-md
|
||||
description: Create, revise, or audit AGENTS.md repository instructions when explicitly requested.
|
||||
---
|
||||
|
||||
# Generate AGENTS.md
|
||||
|
||||
Produce a concise guide that changes the agent's decisions: verified commands with working directories, non-obvious constraints, and links to context needed for specific tasks. Use the project's language and structure; no required headings or line count.
|
||||
|
||||
## Approach
|
||||
|
||||
Inspect the existing guide and the repository evidence relevant to the request. A small edit needs a small investigation; a new guide may need manifests, CI, and contributor docs. Preserve useful user rules. Remove stale or redundant guidance when the evidence supports it; surface unresolved conflicts rather than inventing facts or permissions.
|
||||
|
||||
Keep shared rules at the root. Add a child guide only when that area needs distinct instructions. Link long procedures with their read conditions. Do not copy a repository map, formatter rules, or generic engineering advice into always-loaded context.
|
||||
|
||||
For a new guide, scope changes, command ambiguity, or a substantial audit, consult [the reference](references/agents-md-reference.md). It covers evidence, Codex loading, optional content, and validation limits. Ordinary wording fixes do not need the full reference.
|
||||
|
||||
## Checks and delivery
|
||||
|
||||
Review whether each retained instruction is accurate, scoped, useful, and consistent with the user's authorization. Check only what the change requires. For mechanical checks, run the bundled helper without executing the guide's commands:
|
||||
|
||||
```bash
|
||||
python scripts/validate_agents_md.py --repo /path/to/repo --file /path/to/repo/AGENTS.md
|
||||
```
|
||||
|
||||
Resolve the script relative to this skill directory. Use `--local` for a child guide whose inline paths and commands are local; use `--cwd` when they refer to another directory. Markdown links are always relative to the guide. Warnings require judgment; use `--warnings-as-errors` only for an explicitly strict check.
|
||||
|
||||
Report changed files, relevant evidence, checks performed, and any remaining uncertainty. Passing the helper does not prove command execution, instruction loading, or improved agent performance.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Generate AGENTS.md"
|
||||
short_description: "Create or audit concise repository instructions"
|
||||
default_prompt: "Use $generate-agents-md to create or revise this repository's AGENTS.md using verified facts and task-specific context."
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# AGENTS.md reference
|
||||
|
||||
Use for creation, substantial audits, scope changes, or ambiguous commands. Small wording edits can stay in the entrypoint.
|
||||
|
||||
## Evidence and content selection
|
||||
|
||||
Keep facts that change decisions and are costly to rediscover. Verify commands against the relevant package manifest, build file, CI, or maintained contributor docs; state their working directory and required environment. A listed script exists is weaker evidence than a successful run. If running a command requires unavailable services or credentials, disclose that limit instead of claiming validation.
|
||||
|
||||
Useful content, when supported:
|
||||
|
||||
- A surprising service boundary, generated-file rule, data invariant, or environment trap.
|
||||
- The shortest relevant setup or verification command and its working directory.
|
||||
- A task-conditioned pointer to maintained architecture, schema, or release documentation.
|
||||
- A project-specific completion condition or authorization boundary.
|
||||
|
||||
Omit obvious directory inventories, generic advice, redundant style rules enforced by tools, and facts that are readily read from code. Unverified facts belong in the handoff, not a fill-in template. AGENTS.md is ordinary Markdown: headings and length are choices, not correctness criteria.
|
||||
|
||||
## Scope and loading
|
||||
|
||||
Choose one root guide for shared instructions. Add a child guide when its directory has distinct commands or constraints; keep only the local differences. State whether commands run from the repository root or the child directory. Do not duplicate the parent's full text.
|
||||
|
||||
Codex discovers instructions at startup. Its documented chain includes the global Codex home and project directories from the repository root toward the current working directory. At each level it selects the first applicable file: AGENTS.override.md, AGENTS.md, then configured fallback names. Later, deeper instructions take precedence for conflicts. An override does not concatenate with AGENTS.md in the same directory. Non-empty files form a combined instruction chain subject to a configurable size limit.
|
||||
|
||||
This discovery rule does not guarantee that a newly created child guide will be loaded merely because a running agent edits a file there. Check the target working directory, active session, and host behavior when loading matters. After changing instructions, use a fresh session to verify which files it actually receives; use the target host's current documentation for configuration limits.
|
||||
|
||||
Do not claim an AGENTS.md can override system or developer instructions. Preserve explicit user requirements and established authorization. Host-specific rules belong in a compatibility note only when the user asks for that host. For Claude, Cursor, or Copilot migration, check that host's current primary documentation before adding loader files, imports, or symlinks; do not rename or duplicate guides by default.
|
||||
|
||||
## Editing and boundaries
|
||||
|
||||
Preserve the user's valid conventions and language. Remove a rule only when it is obsolete, redundant, contradicted by verified facts, or covered by the requested cleanup. Surface conflicts whose intended resolution cannot be established.
|
||||
|
||||
Replace unconditional reading lists with conditions: read architecture notes for service-boundary changes, schema notes for schema work, and release notes for a release. Keep a fixed sequence only where deviation breaks correctness or a fragile operation.
|
||||
|
||||
Describe concrete effects and authorization boundaries. Do not add an approval checklist for every dependency, API, CI, or schema edit. State safe autonomous actions only when their safety is verified; a fixture being disposable or a command having no production access must come from evidence. Keep genuinely necessary data, credential, and deployment constraints.
|
||||
|
||||
## Small examples
|
||||
|
||||
These illustrate selection, not a mandatory template:
|
||||
|
||||
```markdown
|
||||
# Project instructions
|
||||
|
||||
Run frontend checks from apps/web with pnpm run check.
|
||||
Database changes use the transaction rules in docs/database.md.
|
||||
The schema client is generated; edit its source schema instead.
|
||||
```
|
||||
|
||||
Only use commands and paths that exist in the target project. Link the real document using a path relative to the guide, and add a child guide only if its local rules justify one.
|
||||
|
||||
## Mechanical validation
|
||||
|
||||
The dependency-free helper checks readable inputs, obvious unfinished prose markers, literal paths and Markdown links, and common package-script commands. It has no fixed heading requirements and never runs a documented command.
|
||||
|
||||
- `--repo`: repository boundary.
|
||||
- `--file`: guide path, resolved from the process working directory.
|
||||
- `--local`: resolve inline paths and commands from the guide's directory instead of the repository root.
|
||||
- `--cwd`: override that inline-path and command base; a relative value is resolved from the repository root. Markdown links still resolve from the guide's directory.
|
||||
- `--warnings-as-errors`: optional strict exit status. Ordinary warnings are review prompts.
|
||||
|
||||
The command check reads only the nearest applicable package.json within the repository, not every package or dependency tree. It recognizes common npm/pnpm/yarn/bun script forms, rtk wrappers, explicit directory flags, and a simple leading cd. Workspace selectors, pipelines, shell expansion, and complex control flow are reported as unverified when recognized; they are not interpreted as a successful check. Run the appropriate project check separately when execution is authorized and useful.
|
||||
|
||||
It does not infer a command's directory from surrounding prose. Use the relevant `--cwd` or an explicit directory flag when checking that command. A root guide describing several package directories may need manual command checks; a warning is not proof that its instructions are wrong. Ambiguous slash-prefixed routes such as /billing are not treated as filesystem references.
|
||||
|
||||
The helper cannot establish command safety, runtime behavior, instruction priority, conflicting prose, or whether the guide improves outcomes. Review those separately. A missing-path warning may refer to a generated artifact; confirm its purpose before changing the guide.
|
||||
|
||||
When changing the helper, run its regression suite from this skill directory: `python -B -m unittest discover -s tests -p 'test_*.py'`.
|
||||
|
||||
## Behavioral acceptance
|
||||
|
||||
For a substantial rewrite, try a representative creation or audit task in an isolated repository. Check that it preserves user constraints, chooses the right scope, uses real commands, avoids invented sections, and gives a usable result. Compare old and new behavior only when both runs were actually captured. Text reduction alone is not evidence of faster or better work.
|
||||
|
||||
## Sources
|
||||
|
||||
Reviewed 2026-09-30. These sources inform the guidance; their examples are not universal requirements.
|
||||
|
||||
- [OpenAI: Rethinking skills and prompts for GPT-6 Astra](https://developers.openai.com/blog/rethinking-skills-and-prompts-for-gpt-6-astra), 2026-09-11: precise discovery, progressive disclosure, conditional reading, proportional boundaries.
|
||||
- [OpenAI: Custom instructions with AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md): Codex discovery and precedence; recheck for host-specific loading questions.
|
||||
- [AGENTS.md](https://agents.md/): plain Markdown, optional sections, scoped instructions.
|
||||
- [Anthropic: Claude Code best practices](https://code.claude.com/docs/en/best-practices): concise persistent context and observed behavior; not a Codex loader specification.
|
||||
- [OpenAI: Testing Agent Skills Systematically with Evals](https://developers.openai.com/blog/eval-skills), 2026-01-22: observable outcomes and targeted evaluation.
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Conservative, dependency-free AGENTS.md checks; never execute guide commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
INLINE = re.compile(r"`([^`\n]+)`")
|
||||
LINK = re.compile(r"!?\[[^\]\n]*\]\(\s*(<[^>\n]+>|[^\s)]+)(?:\s+[^)]*)?\)")
|
||||
MARKER = re.compile(
|
||||
r"^\s*(?:[-*+]\s+|#{1,6}\s+)?(?:TODO|TBD|FIXME|REVIEW_NEEDED)"
|
||||
r"(?:\s*[::].*|\s*)$|\[TODO:[^\]\n]*\]",
|
||||
re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
SHELL_LANGS = {"bash", "sh", "shell", "zsh", "powershell", "pwsh", "ps1", "cmd", "bat", "console", ""}
|
||||
MANAGERS = {"npm", "pnpm", "yarn", "bun"}
|
||||
BUILTINS = {"install", "add", "remove", "exec", "dlx", "create", "init", "run", "publish", "version", "list", "audit", "help", "cache", "config"}
|
||||
|
||||
|
||||
def markdown_parts(text: str) -> tuple[str, list[str]]:
|
||||
"""Separate prose/inline snippets from fenced commands and HTML comments."""
|
||||
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
||||
prose, commands = [], []
|
||||
marker, width, language = "", 0, ""
|
||||
for line in text.splitlines():
|
||||
fence = re.match(r"^\s*(`{3,}|~{3,})(.*)$", line)
|
||||
if fence:
|
||||
run, info = fence.groups()
|
||||
if not marker:
|
||||
marker, width = run[0], len(run)
|
||||
language = info.strip().lower()
|
||||
elif run[0] == marker and len(run) >= width and not info.strip():
|
||||
marker = ""
|
||||
continue
|
||||
if marker:
|
||||
if language in SHELL_LANGS and line.strip() and not line.lstrip().startswith("#"):
|
||||
commands.append(line.strip())
|
||||
else:
|
||||
prose.append(line)
|
||||
plain = "\n".join(prose)
|
||||
return plain, INLINE.findall(plain) + commands
|
||||
|
||||
|
||||
def literal_path(token: str) -> str | None:
|
||||
token = token.strip()
|
||||
if re.match(r"^[a-z][a-z0-9+.-]*:", token, re.IGNORECASE) and not re.match(r"^[A-Za-z]:[\\/]", token):
|
||||
return None
|
||||
if any(char.isspace() for char in token) or any(char in token for char in "$~*?[]{}<>()|;&"):
|
||||
return None
|
||||
token = re.sub(r"(?:#L\d+(?:-L\d+)?|:\d+(?::\d+)?(?:-\d+)?)$", "", token)
|
||||
if token.startswith("/") and not re.search(r"\.[A-Za-z][A-Za-z0-9]*$", token):
|
||||
return None # A bare /billing or /v1/users may be a route, not a file.
|
||||
if "/" in token or "\\" in token or re.search(r"\.[A-Za-z][A-Za-z0-9]*$", token):
|
||||
return token.replace("\\", "/")
|
||||
return None
|
||||
|
||||
|
||||
def path_warning(repo: Path, base: Path, token: str) -> str | None:
|
||||
target = (base / token).resolve()
|
||||
if not target.is_relative_to(repo):
|
||||
return f"External path not verified: {token}"
|
||||
if not target.exists():
|
||||
return f"Referenced path may not exist (base {base.relative_to(repo) or '.'}): {token}"
|
||||
return None
|
||||
|
||||
|
||||
def package_warning(command: str, repo: Path, base: Path) -> str | None:
|
||||
"""Check common script forms only. Unsupported shell/workspace semantics stay unverified."""
|
||||
command = command.removeprefix("$ ").strip()
|
||||
cd = re.match(r"^cd\s+(\"[^\"]+\"|'[^']+'|\S+)\s*&&\s*(.+)$", command)
|
||||
if cd:
|
||||
base = (base / cd[1].strip("\"'")).resolve()
|
||||
command = cd[2]
|
||||
try:
|
||||
tokens = [word.strip("\"'") for word in shlex.split(command, posix=False)]
|
||||
except ValueError:
|
||||
return f"Command quoting not verified: {command}" if re.search(r"\b(?:npm|pnpm|yarn|bun)\b", command) else None
|
||||
if tokens[:1] == ["rtk"]:
|
||||
tokens = tokens[1:]
|
||||
if tokens[:1] == ["proxy"]:
|
||||
tokens = tokens[1:]
|
||||
if not tokens or tokens[0] not in MANAGERS:
|
||||
return None
|
||||
if re.search(r"[|;&$`]|\*|\?", command) or any(t.split("=")[0] in {"--workspace", "--workspaces", "-w", "--filter", "--recursive", "-r"} for t in tokens):
|
||||
return f"Complex or workspace command not verified: {command}"
|
||||
manager, args = tokens[0], tokens[1:]
|
||||
cleaned, index = [], 0
|
||||
while index < len(args):
|
||||
token = args[index]
|
||||
flag = token.split("=", 1)[0]
|
||||
if flag in {"--prefix", "--dir", "-C", "--cwd"}:
|
||||
if "=" in token:
|
||||
directory = token.split("=", 1)[1]
|
||||
elif index + 1 < len(args):
|
||||
index += 1
|
||||
directory = args[index]
|
||||
else:
|
||||
return f"Directory flag not verified: {command}"
|
||||
base = (base / directory).resolve()
|
||||
else:
|
||||
cleaned.append(token)
|
||||
index += 1
|
||||
words = [word for word in cleaned if not word.startswith("-")]
|
||||
if not words:
|
||||
return None
|
||||
action = words[0]
|
||||
if action in {"run", "run-script"}:
|
||||
if len(words) < 2:
|
||||
return None
|
||||
script = words[1]
|
||||
elif manager == "npm":
|
||||
if action not in {"test", "start", "stop", "restart"}:
|
||||
return None
|
||||
script = action
|
||||
elif manager == "bun" and action in {"build", "test"}:
|
||||
return None # Bun has native build/test commands, not package-script aliases.
|
||||
elif action not in BUILTINS:
|
||||
script = action
|
||||
else:
|
||||
return None
|
||||
if not base.is_relative_to(repo) or not base.is_dir():
|
||||
return f"Command working directory not verified: {base}"
|
||||
package = None
|
||||
for directory in (base, *base.parents):
|
||||
candidate = directory / "package.json"
|
||||
if candidate.is_file():
|
||||
package = candidate
|
||||
break
|
||||
if directory == repo:
|
||||
break
|
||||
if package is None:
|
||||
return f"Package command not verified (no package.json from {base.relative_to(repo)}): {command}"
|
||||
try:
|
||||
data = json.loads(package.read_text(encoding="utf-8-sig"))
|
||||
scripts = data.get("scripts", {})
|
||||
if not isinstance(scripts, dict):
|
||||
scripts = {}
|
||||
except (OSError, UnicodeError, ValueError, AttributeError) as exc:
|
||||
return f"Cannot read {package.relative_to(repo)}: {exc}"
|
||||
if script not in scripts:
|
||||
if "--if-present" in cleaned:
|
||||
return None
|
||||
if manager == "npm" and action in {"start", "restart", "stop"}:
|
||||
return f"npm lifecycle fallback not verified: {command}"
|
||||
return f"Missing package script '{script}' in {package.relative_to(repo)}: {command}"
|
||||
return None
|
||||
|
||||
|
||||
def validate(args: argparse.Namespace) -> tuple[list[str], list[str]]:
|
||||
repo, guide = Path(args.repo).resolve(), Path(args.file).resolve()
|
||||
if not repo.is_dir():
|
||||
return [f"Repository directory does not exist: {repo}"], []
|
||||
if not guide.is_file() or not guide.is_relative_to(repo):
|
||||
return [f"Guide must be a file inside the repository: {guide}"], []
|
||||
base = guide.parent if args.local else repo
|
||||
if getattr(args, "cwd", None):
|
||||
base = (repo / args.cwd).resolve()
|
||||
if not base.is_dir() or not base.is_relative_to(repo):
|
||||
return [f"Working directory must exist inside the repository: {base}"], []
|
||||
try:
|
||||
text = guide.read_text(encoding="utf-8-sig")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
return [f"Cannot read guide: {exc}"], []
|
||||
prose, snippets = markdown_parts(text)
|
||||
errors, warnings = [], []
|
||||
if not prose.strip() and not snippets:
|
||||
errors.append("Guide has no instruction content")
|
||||
for marker in MARKER.finditer(INLINE.sub("", prose)):
|
||||
errors.append(f"Unfinished prose marker: {marker.group().strip()}")
|
||||
for match in LINK.finditer(INLINE.sub("", prose)):
|
||||
token = unquote(match[1].strip("<>").split("#", 1)[0])
|
||||
if not token or re.match(r"^[a-z][a-z0-9+.-]*:", token, re.IGNORECASE):
|
||||
continue
|
||||
warning = path_warning(repo, guide.parent, token)
|
||||
if warning:
|
||||
warnings.append(warning)
|
||||
for snippet in snippets:
|
||||
token = literal_path(snippet)
|
||||
if token:
|
||||
warning = path_warning(repo, base, token)
|
||||
if warning:
|
||||
warnings.append(warning)
|
||||
warning = package_warning(snippet, repo, base)
|
||||
if warning:
|
||||
warnings.append(warning)
|
||||
return list(dict.fromkeys(errors)), list(dict.fromkeys(warnings))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repo", required=True, help="Repository boundary")
|
||||
parser.add_argument("--file", required=True, help="Guide path, relative to process cwd")
|
||||
parser.add_argument("--local", action="store_true", help="Inline paths/commands are relative to guide directory")
|
||||
parser.add_argument("--cwd", help="Inline-path/command base, relative to repo or absolute")
|
||||
parser.add_argument("--warnings-as-errors", action="store_true", help="Fail on heuristic warnings as well as errors")
|
||||
args = parser.parse_args()
|
||||
errors, warnings = validate(args)
|
||||
for error in errors:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
for warning in warnings:
|
||||
print(f"WARNING: {warning}", file=sys.stderr)
|
||||
if errors or (args.warnings_as_errors and warnings):
|
||||
return 1
|
||||
print("Mechanical checks passed" + (" with warnings; review required" if warnings else ""))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
"""Behavioral regressions using isolated repositories; no third-party dependencies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "validate_agents_md.py"
|
||||
SPEC = importlib.util.spec_from_file_location("guide_validator", SCRIPT)
|
||||
validator = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(validator)
|
||||
|
||||
|
||||
class GuideValidationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.repo = Path(self.temp.name).resolve()
|
||||
self.guide = self.repo / "AGENTS.md"
|
||||
|
||||
def write(self, relative, text):
|
||||
path = self.repo / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def package(self, directory, scripts):
|
||||
self.write(str(Path(directory) / "package.json"), json.dumps({"scripts": scripts}))
|
||||
|
||||
def check(self, text, *, guide="AGENTS.md", local=False, cwd=None):
|
||||
path = self.write(guide, text)
|
||||
return validator.validate(argparse.Namespace(repo=str(self.repo), file=str(path), local=local, cwd=cwd))
|
||||
|
||||
def test_chinese_guide_has_no_heading_contract(self):
|
||||
self.assertEqual(self.check("# 项目约定\n不要覆盖已有用户修改。\n"), ([], []))
|
||||
|
||||
def test_html_jsx_and_literal_markers_are_not_placeholders(self):
|
||||
self.assertEqual(self.check("# Guide\n<!-- maintained -->\nUse `<Component>` and `TODO` only in examples.\n"), ([], []))
|
||||
|
||||
def test_real_unfinished_marker_fails(self):
|
||||
errors, warnings = self.check("# Guide\n- TODO: add the actual build command\n")
|
||||
self.assertTrue(errors)
|
||||
self.assertFalse(warnings)
|
||||
|
||||
def test_empty_guide_fails(self):
|
||||
self.assertTrue(self.check("<!-- nothing here -->")[0])
|
||||
|
||||
def test_command_and_line_reference_are_not_whole_paths(self):
|
||||
self.write("scripts/check.py", "print('ok')\n")
|
||||
self.write("src/main.py", "pass\n")
|
||||
self.assertEqual(self.check("Run `python scripts/check.py`; see `src/main.py:42`."), ([], []))
|
||||
|
||||
def test_missing_literal_path_warns(self):
|
||||
self.assertTrue(self.check("See `src/missing.py`.")[1])
|
||||
|
||||
def test_route_constraint_is_not_an_external_file_warning(self):
|
||||
self.assertEqual(self.check("Keep route `/billing`; preserve `/v1/users`."), ([], []))
|
||||
|
||||
def test_child_paths_and_links_have_correct_bases(self):
|
||||
self.write("apps/web/src/main.py", "pass\n")
|
||||
self.write("docs/api.md", "# API\n")
|
||||
self.assertEqual(self.check("Use `src/main.py` and [API](../../docs/api.md).", guide="apps/web/AGENTS.md", local=True), ([], []))
|
||||
|
||||
def test_explicit_cwd_does_not_move_markdown_links(self):
|
||||
self.write("apps/web/src/main.py", "pass\n")
|
||||
self.write("docs/api.md", "# API\n")
|
||||
self.assertEqual(self.check("Use `src/main.py` and [API](docs/api.md).", cwd="apps/web"), ([], []))
|
||||
|
||||
def test_missing_markdown_link_and_url_encoded_spaces(self):
|
||||
self.write("docs/My Guide.md", "# Details\n")
|
||||
self.assertEqual(self.check("[Details](docs/My%20Guide.md#notes)"), ([], []))
|
||||
self.assertTrue(self.check("[Details](docs/missing.md)")[1])
|
||||
|
||||
def test_missing_package_script_cannot_borrow_from_sibling(self):
|
||||
self.package("apps/a", {})
|
||||
self.package("apps/b", {"test": "echo ok"})
|
||||
errors, warnings = self.check("Run `npm run test`.", guide="apps/a/AGENTS.md", local=True)
|
||||
self.assertFalse(errors)
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn("Missing package script", warnings[0])
|
||||
|
||||
def test_no_package_does_not_scan_dependency_tree(self):
|
||||
self.package("node_modules/dependency", {"test": "echo ok"})
|
||||
errors, warnings = self.check("Run `npm run test`.")
|
||||
self.assertFalse(errors)
|
||||
self.assertIn("no package.json", warnings[0])
|
||||
|
||||
def test_fenced_commands_cd_wrappers_and_directory_flags(self):
|
||||
self.package("apps/web", {"check": "echo ok"})
|
||||
commands = ["cd apps/web && npm run check", "rtk proxy pnpm -C apps/web run check", "npm --prefix=apps/web run check", "yarn --cwd apps/web check"]
|
||||
for command in commands:
|
||||
with self.subTest(command=command):
|
||||
self.assertEqual(self.check("```sh\n" + command + "\n```\n"), ([], []))
|
||||
|
||||
def test_unsupported_workspace_or_pipeline_is_not_claimed_verified(self):
|
||||
self.package(".", {"test": "echo ok"})
|
||||
for command in ["npm run test --workspace apps/web", "npm run test | cat"]:
|
||||
with self.subTest(command=command):
|
||||
self.assertIn("not verified", self.check("`" + command + "`")[1][0])
|
||||
|
||||
def test_validation_never_executes_documented_commands(self):
|
||||
self.package(".", {"test": "python -c \"open('executed', 'w').close()\""})
|
||||
self.assertEqual(self.check("Run `npm run test`."), ([], []))
|
||||
self.assertFalse((self.repo / "executed").exists())
|
||||
|
||||
def test_bare_build_alias_and_bun_native_command_differ(self):
|
||||
self.package(".", {})
|
||||
for manager in ["yarn", "pnpm"]:
|
||||
with self.subTest(manager=manager):
|
||||
self.assertIn("Missing package script", self.check("`" + manager + " build`")[1][0])
|
||||
self.assertEqual(self.check("`bun build src/index.ts`"), ([], []))
|
||||
|
||||
def test_optional_missing_script_and_malformed_package(self):
|
||||
self.package(".", {})
|
||||
self.assertEqual(self.check("`npm run --if-present check`"), ([], []))
|
||||
self.write("package.json", "not json")
|
||||
self.assertIn("Cannot read", self.check("`npm run check`")[1][0])
|
||||
|
||||
def test_outside_working_directory_fails(self):
|
||||
self.assertTrue(self.check("# Guide\nKeep user work.\n", cwd="..")[0])
|
||||
|
||||
def test_cli_warning_and_strict_exit_status(self):
|
||||
self.write("AGENTS.md", "See `missing.py`.\n")
|
||||
command = [sys.executable, "-B", str(SCRIPT), "--repo", str(self.repo), "--file", str(self.guide)]
|
||||
ordinary = subprocess.run(command, capture_output=True, text=True)
|
||||
strict = subprocess.run(command + ["--warnings-as-errors"], capture_output=True, text=True)
|
||||
self.assertEqual(ordinary.returncode, 0)
|
||||
self.assertIn("WARNING", ordinary.stderr)
|
||||
self.assertEqual(strict.returncode, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user