qa-agent-kit 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qa_agent_kit-0.6.0.dist-info/METADATA +157 -0
- qa_agent_kit-0.6.0.dist-info/RECORD +32 -0
- qa_agent_kit-0.6.0.dist-info/WHEEL +4 -0
- qa_agent_kit-0.6.0.dist-info/entry_points.txt +6 -0
- qa_agent_kit-0.6.0.dist-info/licenses/LICENSE +21 -0
- qakit/AGENTS.md +48 -0
- qakit/__init__.py +21 -0
- qakit/_cli.py +46 -0
- qakit/docs/README.md +14 -0
- qakit/docs/agents.md +40 -0
- qakit/docs/case-studies.md +146 -0
- qakit/docs/device-qa-protocol.md +426 -0
- qakit/docs/getting-started.md +100 -0
- qakit/docs/ko/device-qa-protocol.md +433 -0
- qakit/docs/manifest.md +96 -0
- qakit/docs/mutation-testing.md +69 -0
- qakit/docs/prose-qa.md +49 -0
- qakit/docs/pypi.md +40 -0
- qakit/docs/report.md +64 -0
- qakit/skills/README.md +51 -0
- qakit/skills/device-qa/SKILL.md +87 -0
- qakit/skills/prose-qa/SKILL.md +49 -0
- qakit/skills/qa-rigor/SKILL.md +105 -0
- qakit/tools/device_lock.py +151 -0
- qakit/tools/device_qa.py +838 -0
- qakit/tools/install_skills.py +120 -0
- qakit/tools/mutate.py +684 -0
- qakit/tools/prose_check.py +291 -0
- qakit/tools/recording_frames.swift +78 -0
- qakit/tools/release_checks.py +287 -0
- qakit/tools/screenshot_contrast.swift +42 -0
- qakit/tools/shots_gif.py +81 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Link the QAKit skills into the folder each AI coding tool reads, so one checkout serves them all.
|
|
3
|
+
|
|
4
|
+
python3 tools/install_skills.py --target claude --scope project # <cwd>/.claude/skills/<skill> -> this checkout
|
|
5
|
+
python3 tools/install_skills.py --target all --scope user # ~/.claude/skills, ~/.agents/skills, ~/.cursor/rules, ~/.gemini
|
|
6
|
+
python3 tools/install_skills.py --target codex --scope project --project /path/to/repo
|
|
7
|
+
python3 tools/install_skills.py --target claude --scope project --uninstall
|
|
8
|
+
|
|
9
|
+
Targets and what they get:
|
|
10
|
+
claude .claude/skills/<skill> -> symlink to skills/<skill> (Claude Code loads SKILL.md from there)
|
|
11
|
+
codex .agents/skills/<skill> -> symlink (Codex CLI skills folder); plus AGENTS.md gets a pointer line when the project has one
|
|
12
|
+
cursor .cursor/rules/qakit-<skill>.mdc -> a rule file that tells Cursor to read the SKILL.md
|
|
13
|
+
gemini GEMINI.md (project) or ~/.gemini/GEMINI.md (user) gets a pointer line
|
|
14
|
+
Existing files that are not QAKit links are left alone and reported. Idempotent.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
KIT = Path(__file__).resolve().parents[1]
|
|
25
|
+
SKILLS = sorted(p.parent.name for p in (KIT / "skills").glob("*/SKILL.md"))
|
|
26
|
+
POINTER = "QAKit (QA agent kit): read {path} before QA work; procedures in {skills}.\n"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def targets_for(name: str, scope: str, project: Path, home: Path) -> list[tuple[str, Path, Path | None]]:
|
|
30
|
+
"""(kind, destination, source) per skill: kind 'link' symlinks a folder, 'rule' writes a Cursor rule, 'pointer' appends a line."""
|
|
31
|
+
base = project if scope == "project" else home
|
|
32
|
+
out: list[tuple[str, Path, Path | None]] = []
|
|
33
|
+
if name == "claude":
|
|
34
|
+
out += [("link", base / ".claude/skills" / s, KIT / "skills" / s) for s in SKILLS]
|
|
35
|
+
elif name == "codex":
|
|
36
|
+
out += [("link", base / ".agents/skills" / s, KIT / "skills" / s) for s in SKILLS]
|
|
37
|
+
if scope == "project":
|
|
38
|
+
out.append(("pointer", base / "AGENTS.md", None))
|
|
39
|
+
elif name == "cursor":
|
|
40
|
+
out += [("rule", base / ".cursor/rules" / f"qakit-{s}.mdc", KIT / "skills" / s / "SKILL.md") for s in SKILLS]
|
|
41
|
+
elif name == "gemini":
|
|
42
|
+
out.append(("pointer", base / "GEMINI.md" if scope == "project" else home / ".gemini/GEMINI.md", None))
|
|
43
|
+
else:
|
|
44
|
+
raise SystemExit(f"unknown target {name}")
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def rule_text(skill_md: Path) -> str:
|
|
49
|
+
name = skill_md.parent.name
|
|
50
|
+
return f"---\ndescription: QAKit {name} procedure\nalwaysApply: false\n---\nBefore QA work of this kind, read and follow {skill_md} (QAKit, {KIT}).\n"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pointer_line() -> str:
|
|
54
|
+
return POINTER.format(path=KIT / "AGENTS.md", skills=KIT / "skills")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def apply(kind: str, dest: Path, source: Path | None, uninstall: bool) -> str:
|
|
58
|
+
if kind == "link":
|
|
59
|
+
if dest.is_symlink():
|
|
60
|
+
if os.path.realpath(dest) != os.path.realpath(source):
|
|
61
|
+
return f"skip {dest} (symlink to something else)"
|
|
62
|
+
if uninstall:
|
|
63
|
+
dest.unlink()
|
|
64
|
+
return f"removed {dest}"
|
|
65
|
+
return f"ok {dest}"
|
|
66
|
+
if dest.exists():
|
|
67
|
+
return f"skip {dest} (exists and is not a QAKit link)"
|
|
68
|
+
if uninstall:
|
|
69
|
+
return f"absent {dest}"
|
|
70
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
dest.symlink_to(source, target_is_directory=True) # mutation-ok: the flag only matters on Windows
|
|
72
|
+
return f"linked {dest} -> {source}"
|
|
73
|
+
if kind == "rule":
|
|
74
|
+
text = rule_text(source)
|
|
75
|
+
if uninstall:
|
|
76
|
+
if dest.is_file() and "QAKit" in dest.read_text(encoding="utf-8"):
|
|
77
|
+
dest.unlink()
|
|
78
|
+
return f"removed {dest}"
|
|
79
|
+
return f"absent {dest}"
|
|
80
|
+
if dest.is_file() and dest.read_text(encoding="utf-8") == text:
|
|
81
|
+
return f"ok {dest}"
|
|
82
|
+
if dest.is_file() and "QAKit" not in dest.read_text(encoding="utf-8"):
|
|
83
|
+
return f"skip {dest} (exists and is not a QAKit rule)"
|
|
84
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
dest.write_text(text, encoding="utf-8")
|
|
86
|
+
return f"wrote {dest}"
|
|
87
|
+
if kind == "pointer":
|
|
88
|
+
line = pointer_line()
|
|
89
|
+
existing = dest.read_text(encoding="utf-8") if dest.is_file() else ""
|
|
90
|
+
if uninstall:
|
|
91
|
+
if line in existing:
|
|
92
|
+
dest.write_text(existing.replace(line, ""), encoding="utf-8")
|
|
93
|
+
return f"removed pointer in {dest}"
|
|
94
|
+
return f"absent {dest}"
|
|
95
|
+
if line in existing:
|
|
96
|
+
return f"ok {dest}"
|
|
97
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
dest.write_text(existing + ("\n" if existing and not existing.endswith("\n") else "") + line, encoding="utf-8")
|
|
99
|
+
return f"added pointer in {dest}"
|
|
100
|
+
raise SystemExit(kind)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main(argv: list[str] | None = None) -> int:
|
|
104
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
105
|
+
parser.add_argument("--target", action="append", choices=["claude", "codex", "cursor", "gemini", "all"], required=True)
|
|
106
|
+
parser.add_argument("--scope", choices=["project", "user"], default="project")
|
|
107
|
+
parser.add_argument("--project", default=".", help="project folder for --scope project (default: cwd)")
|
|
108
|
+
parser.add_argument("--home", default=str(Path.home()), help=argparse.SUPPRESS)
|
|
109
|
+
parser.add_argument("--uninstall", action="store_true")
|
|
110
|
+
args = parser.parse_args(argv)
|
|
111
|
+
names = ["claude", "codex", "cursor", "gemini"] if "all" in args.target else args.target
|
|
112
|
+
project, home = Path(args.project).resolve(), Path(args.home).resolve()
|
|
113
|
+
for name in names:
|
|
114
|
+
for kind, dest, source in targets_for(name, args.scope, project, home):
|
|
115
|
+
print(apply(kind, dest, source, args.uninstall))
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
sys.exit(main()) # mutation-ok: entry point, main() always returns 0
|