qa-engineer 0.9.0 → 0.9.2
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.
- package/COMPATIBILITY.md +25 -15
- package/LICENSE +1 -1
- package/README.md +22 -4
- package/package.json +4 -3
- package/packages/installer/README.md +1 -1
- package/packages/installer/lib/agents/registry.mjs +46 -0
- package/packages/installer/lib/commands/doctor.mjs +6 -6
- package/packages/installer/lib/commands/install.mjs +32 -5
- package/packages/installer/lib/commands/onboard.mjs +2 -2
- package/packages/installer/lib/core/config.mjs +1 -1
- package/packages/installer/lib/core/lockfile.mjs +1 -1
- package/packages/installer/lib/core/manifest.mjs +3 -0
- package/packages/installer/lib/core/wrappers.mjs +4 -4
- package/packages/installer/lib/ui/theme.mjs +1 -1
- package/packages/installer/package.json +1 -1
- package/packages/installer/schemas/qa-lock.schema.json +1 -1
- package/packages/installer/schemas/qa.config.schema.json +1 -1
- package/shared/analysis/lib/qa_analysis/branding.json +1 -1
- package/shared/analysis/lib/qa_analysis/cli.py +15 -0
- package/shared/analysis/lib/qa_analysis/har.py +32 -3
- package/shared/analysis/lib/qa_analysis/junit.py +25 -1
- package/shared/analysis/lib/qa_analysis/redaction.py +10 -2
- package/shared/analysis/lib/qa_analysis/report_html.py +781 -0
- package/skills/qa/README.md +1 -1
- package/skills/qa/SKILL.md +1 -1
- package/skills/qa-api/references/deterministic-tooling.md +2 -0
- package/skills/qa-api/references/evidence-and-reporting.md +29 -4
- package/skills/qa-audit/references/deterministic-tooling.md +2 -0
- package/skills/qa-audit/references/evidence-and-reporting.md +29 -4
- package/skills/qa-debug/references/deterministic-tooling.md +2 -0
- package/skills/qa-debug/references/evidence-and-reporting.md +29 -4
- package/skills/qa-example/SKILL.md +1 -1
- package/skills/qa-example/contracts/self-check-report.schema.json +1 -1
- package/skills/qa-explore/SKILL.md +22 -5
- package/skills/qa-explore/contracts/explore-result.schema.json +26 -2
- package/skills/qa-explore/references/deterministic-tooling.md +130 -0
- package/skills/qa-explore/references/evidence-and-reporting.md +29 -4
- package/skills/qa-explore/references/report-pipeline.md +89 -8
- package/skills/qa-fix/references/deterministic-tooling.md +2 -0
- package/skills/qa-fix/references/evidence-and-reporting.md +29 -4
- package/skills/qa-flaky/references/deterministic-tooling.md +2 -0
- package/skills/qa-flaky/references/evidence-and-reporting.md +29 -4
- package/skills/qa-generate/references/evidence-and-reporting.md +29 -4
- package/skills/qa-init/references/deterministic-tooling.md +2 -0
- package/skills/qa-init/references/evidence-and-reporting.md +29 -4
- package/skills/qa-report/SKILL.md +3 -2
- package/skills/qa-report/references/deterministic-tooling.md +2 -0
- package/skills/qa-report/references/evidence-and-reporting.md +29 -4
- package/skills/qa-review/references/evidence-and-reporting.md +29 -4
- package/skills/qa-run/references/deterministic-tooling.md +2 -0
- package/skills/qa-run/references/evidence-and-reporting.md +29 -4
- package/packages/installer/lib/core/schema-validate.mjs +0 -142
package/COMPATIBILITY.md
CHANGED
|
@@ -6,7 +6,14 @@ This document records which AI coding agents the pack targets, how each one disc
|
|
|
6
6
|
|
|
7
7
|
## Release compatibility
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
| Pack version | Released | Discovery paths verified against agent docs | Live agent runs |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| 0.9.0, 0.9.1 | 2026-07-28 | Not systematically re-read for the release | Claude Code, Cursor (manual, by the maintainer) |
|
|
12
|
+
| 0.9.2 | 2026-07-28 | Yes — every host's own documentation, re-read on 2026-07-28 (sources in the matrix below) | Claude Code, Cursor (manual, by the maintainer) |
|
|
13
|
+
|
|
14
|
+
"Verified" in the third column means the discovery paths in the matrix below were read off each host's own documentation on that date and are asserted against the installer by `packages/installer/test/hosts.test.mjs`, which fails if the installer writes to a path that host does not document reading. It does **not** mean the pack was executed inside every host: that is the Tier 1 bar, and only Claude Code and Cursor have had a real run.
|
|
15
|
+
|
|
16
|
+
Documentation is re-read at each release. A test can pin the installer to a recorded claim; it cannot notice that a vendor moved a directory, so that check is a release step performed by a person.
|
|
10
17
|
|
|
11
18
|
## The standard this pack targets
|
|
12
19
|
|
|
@@ -14,23 +21,26 @@ Skills are authored against the open [Agent Skills specification](https://agents
|
|
|
14
21
|
|
|
15
22
|
## Target agent matrix
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
Each row's discovery paths were read off that host's own documentation on **2026-07-28** and are recorded in the installer's agent registry, where a test asserts the installer writes to one of them. The **bold** path is the one `qa install` targets.
|
|
25
|
+
|
|
26
|
+
| Agent | Paths this host reads (project) | Pack installs to | How you reach a skill | Source | Planned tier |
|
|
27
|
+
| --- | --- | --- | --- | --- | --- |
|
|
28
|
+
| Claude Code | `.claude/skills/` | **`.claude/skills/`** | `/qa-explore`, or describe the task | [docs](https://code.claude.com/docs/en/skills) | Tier 1 |
|
|
29
|
+
| OpenAI Codex CLI | `.agents/skills/` — at the working directory, its parent, and the repo root. There is **no** `.codex/skills/` | **`.agents/skills/`** | `$qa-explore`, or `/skills` to browse | [docs](https://developers.openai.com/codex/skills) | Tier 1 |
|
|
30
|
+
| OpenCode | `.agents/skills/`, `.opencode/skills/`, `.claude/skills/` | **`.agents/skills/`** | `/qa-explore` (generated command), or the agent's own `skill` tool | [docs](https://opencode.ai/docs/skills/) | Tier 1 |
|
|
31
|
+
| Cursor | `.agents/skills/`, `.cursor/skills/`, plus Claude and Codex directories | **`.agents/skills/`** | `/` in Agent chat, then pick the skill | [docs](https://cursor.com/docs/skills) | Tier 2 |
|
|
32
|
+
| Antigravity | `.agents/skills/` (default), `.agent/skills/` (legacy) | **`.agents/skills/`** | describe the task — skills auto-activate | [docs](https://antigravity.google/docs/skills) | Tier 2 |
|
|
33
|
+
| GitHub Copilot | `.agents/skills/`, `.github/skills/` | **`.agents/skills/`** | `/qa-explore` in Copilot Chat (generated prompt file) | [docs](https://docs.github.com/en/copilot) | Tier 2 |
|
|
34
|
+
| Gemini CLI | `.agents/skills/`, `.gemini/skills/` | **`.agents/skills/`** | `/qa-explore` (generated command) | [docs](https://google-gemini.github.io/gemini-cli/) | Tier 2 |
|
|
35
|
+
| Kimi (Agent Skills copy) / other spec-compliant hosts | `.agents/skills/` | **`.agents/skills/`** | product's own surface | [spec](https://agentskills.io/specification) | Tier 2 |
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
| --- | --- | --- | --- | --- |
|
|
21
|
-
| Claude Code | Yes (format originator) | `.claude/skills/` (project), plugins | `/skill-name`, auto-activation | Tier 1 |
|
|
22
|
-
| OpenAI Codex CLI | Yes | `.agents/skills/` | `$skill-name` mention, `/skills` menu, auto-activation | Tier 1 |
|
|
23
|
-
| OpenCode | Yes | `.agents/skills/`, `.opencode/skills/` | native `skill` tool (auto); slash via generated commands | Tier 1 |
|
|
24
|
-
| Cursor | Yes (2.4+) | `.agents/skills/`, `.cursor/skills/` | `/skill-name`, auto-activation | Tier 2 |
|
|
25
|
-
| GitHub Copilot | Yes (VS Code, JetBrains, CLI, cloud) | `.agents/skills/`, `.github/skills/` | `/skill-name` in VS Code; auto-activation elsewhere | Tier 2 |
|
|
26
|
-
| Gemini CLI | Yes | `.agents/skills/`, `.gemini/skills/` | auto-activation; slash via generated commands | Tier 2 |
|
|
27
|
-
| Antigravity | Yes | `.agents/skills/` | auto-activation; slash via workflows (*path unverified*) | Tier 2 |
|
|
28
|
-
| Kimi / other Agent Skills hosts | Yes (copy) | `.agents/skills/` (or product-specific path) | auto-activation / product slash | Tier 2 |
|
|
37
|
+
Three facts follow from that table and shape the design:
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
1. **One path serves seven of the eight hosts.** Everything except Claude Code reads `.agents/skills/`, so `qa install` writes the same canonical skill set there — copies, never conversions. Claude Code gets `.claude/skills/` as well, and a project where only Claude Code is detected still receives the shared path, so opening the same repository in Cursor or Codex tomorrow needs no reinstall.
|
|
40
|
+
2. **Auto-activation is the only channel every host shares.** Codex takes `$qa-explore`, Cursor matches on `/`, OpenCode's agent calls a `skill` tool. `qa install` prints the convention for the host it detected, because a user who types the wrong one has no way to tell a wrong keystroke from a failed install.
|
|
41
|
+
3. **Slash ergonomics on Gemini CLI, OpenCode, and Copilot need a thin generated wrapper.** Those wrappers are rendered from skill frontmatter alone (ADR-0002), capped at 15 lines, and carry no knowledge. Antigravity's workflow directory (`.agents/workflows/`) is *not* documented by Google, so those wrappers are generated only on explicit `--agent antigravity`; skills themselves work there without them.
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
2. Description-driven auto-activation is the only invocation channel available on all primary agents. Slash-command ergonomics on Gemini CLI, OpenCode, and Antigravity require thin generated wrappers from the installer (`qa install`).
|
|
43
|
+
**Antigravity detection is deliberately conservative.** Its skills live at `.agents/skills/`, which is also the path this installer creates for every host — so detecting Antigravity from `.agents/` would mean detecting it because the pack had run once before. An earlier release did exactly that and reported a host that was never there. Skills work in Antigravity either way; only the optional workflow wrappers need `--agent antigravity`.
|
|
34
44
|
|
|
35
45
|
## Support tiers
|
|
36
46
|
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 QA
|
|
3
|
+
Copyright (c) 2026 QA Engineer Pack contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# QA
|
|
1
|
+
# QA Engineer Pack
|
|
2
2
|
|
|
3
3
|
> Teach your AI coding assistant to work like a senior QA engineer — and stop it from telling you tests pass when they don't.
|
|
4
4
|
|
|
@@ -256,9 +256,27 @@ qa-artifacts/
|
|
|
256
256
|
|
|
257
257
|
Most teams **do** commit `.agents/skills/` and `qa-lock.json`, so everyone on the team gets the same skills.
|
|
258
258
|
|
|
259
|
-
##
|
|
259
|
+
## How you type a command in your assistant
|
|
260
|
+
|
|
261
|
+
Type these in your AI assistant's **chat**, not the terminal. The keystroke differs
|
|
262
|
+
by assistant, and typing the wrong one looks exactly like a failed install — so
|
|
263
|
+
`qa install` prints the right one for whatever it detected. For reference:
|
|
260
264
|
|
|
261
|
-
|
|
265
|
+
| Assistant | How to reach a skill |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| Claude Code | `/qa-explore`, or just describe the task |
|
|
268
|
+
| Cursor | type `/` in Agent chat, then pick the skill from the list |
|
|
269
|
+
| OpenAI Codex CLI | `$qa-explore` — or `/skills` to browse them |
|
|
270
|
+
| OpenCode | `/qa-explore`, or describe the task and the agent loads the skill itself |
|
|
271
|
+
| Antigravity | describe the task; skills activate on their own |
|
|
272
|
+
| GitHub Copilot / Gemini CLI | `/qa-explore` |
|
|
273
|
+
|
|
274
|
+
Every one of them also activates a skill on its own when your request matches — you
|
|
275
|
+
never *have* to remember a command. "Find bugs on my login page at localhost:3000"
|
|
276
|
+
reaches `/qa-explore` in all of them. Full details and the source for each path:
|
|
277
|
+
[COMPATIBILITY.md](COMPATIBILITY.md).
|
|
278
|
+
|
|
279
|
+
## All twelve commands
|
|
262
280
|
|
|
263
281
|
| Command | What it does | Changes your files? |
|
|
264
282
|
| --- | --- | --- |
|
|
@@ -503,6 +521,6 @@ One volunteer maintainer, no SLA. [MAINTAINERS.md](MAINTAINERS.md) says what tha
|
|
|
503
521
|
|
|
504
522
|
## Licence
|
|
505
523
|
|
|
506
|
-
[MIT](LICENSE) © QA
|
|
524
|
+
[MIT](LICENSE) © QA Engineer Pack contributors. Use it commercially, fork it, modify it.
|
|
507
525
|
|
|
508
526
|
Designed and developed by [Abisheik](https://abisheik.dev).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qa-engineer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "A vendor-neutral Agent Skills pack that teaches AI coding agents to work like senior QA automation engineers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,18 +48,19 @@
|
|
|
48
48
|
"!**/tests"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"qa": "node packages/installer/bin/qa.mjs",
|
|
52
51
|
"eval:live": "python3 tests/evals/run_live.py --baseline tests/evals/baselines/reference.json",
|
|
53
52
|
"prepublishOnly": "npm run validate:release && npm run validate:doc-claims && npm run validate:docs-commands && npm test && npm run validate:python && npm run validate:evals",
|
|
53
|
+
"qa": "node packages/installer/bin/qa.mjs",
|
|
54
54
|
"release:checksums": "node scripts/release/release-notes.mjs --checksums",
|
|
55
55
|
"release:notes": "node scripts/release/release-notes.mjs",
|
|
56
|
-
"test": "node --test packages/installer/test/*.test.mjs",
|
|
56
|
+
"test": "node --test packages/installer/test/*.test.mjs packages/engine/test/*.test.mjs",
|
|
57
57
|
"validate:architecture": "node scripts/check-architecture-fitness.mjs",
|
|
58
58
|
"validate:branding": "node scripts/check-branding.mjs",
|
|
59
59
|
"validate:bundle": "python3 scripts/bundle_python.py --check",
|
|
60
60
|
"validate:context": "cd shared/analysis/lib && python3 -m qa_analysis.cli context --path tests/fixtures/valid-context.md",
|
|
61
61
|
"validate:doc-claims": "node scripts/check-doc-claims.mjs",
|
|
62
62
|
"validate:docs-commands": "node scripts/check-docs-commands.mjs",
|
|
63
|
+
"validate:engine-parity": "node scripts/check-engine-parity.mjs",
|
|
63
64
|
"validate:evals": "python3 tests/evals/run_evals.py",
|
|
64
65
|
"validate:keywords": "node scripts/check-keywords.mjs",
|
|
65
66
|
"validate:knowledge": "node scripts/check-knowledge.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @qa-engineer/installer
|
|
2
2
|
|
|
3
|
-
Copy-based installer for the QA
|
|
3
|
+
Copy-based installer for the QA Engineer Pack. Installs Agent Skills into `.agents/skills/` and `.claude/skills/`, writes `qa-lock.json`, and generates thin slash wrappers for agents that need them. **No skill code runs at install time.**
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
@@ -14,9 +14,18 @@ import { SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR } from '../constants.mjs';
|
|
|
14
14
|
* @property {string} skillsDir project-relative skills discovery path
|
|
15
15
|
* @property {string|null} wrapperFormat wrappers.mjs renderer key, or null
|
|
16
16
|
* @property {string|null} wrapperDir where wrappers are written, or null
|
|
17
|
+
* @property {readonly string[]} discovery every project path this host reads skills from
|
|
18
|
+
* @property {string} docs the primary documentation the discovery list was read from
|
|
19
|
+
* @property {string} invoke how a user reaches a skill in this host, in their words
|
|
17
20
|
* @property {(root:string)=>boolean} detect
|
|
18
21
|
*/
|
|
19
22
|
|
|
23
|
+
// `discovery` is what makes an install correct rather than hopeful: writing skills
|
|
24
|
+
// to a path the host does not read produces a silent no-op — the files are there,
|
|
25
|
+
// the agent never sees them, and the user concludes the pack does not work. Each
|
|
26
|
+
// list below was read off that host's own documentation on 2026-07-28 (`docs`), and
|
|
27
|
+
// a test asserts `skillsDir` is a member of it. See COMPATIBILITY.md.
|
|
28
|
+
|
|
20
29
|
/** @type {AgentDef[]} */
|
|
21
30
|
export const AGENTS = Object.freeze([
|
|
22
31
|
{
|
|
@@ -26,6 +35,9 @@ export const AGENTS = Object.freeze([
|
|
|
26
35
|
skillsDir: CLAUDE_SKILLS_DIR,
|
|
27
36
|
wrapperFormat: null,
|
|
28
37
|
wrapperDir: null,
|
|
38
|
+
discovery: Object.freeze(['.claude/skills']),
|
|
39
|
+
docs: 'https://code.claude.com/docs/en/skills',
|
|
40
|
+
invoke: 'type /qa-explore, or just describe the task — skills auto-activate',
|
|
29
41
|
detect: (root) =>
|
|
30
42
|
fs.existsSync(path.join(root, '.claude')) ||
|
|
31
43
|
fs.existsSync(path.join(root, 'CLAUDE.md')),
|
|
@@ -35,6 +47,10 @@ export const AGENTS = Object.freeze([
|
|
|
35
47
|
name: 'Cursor',
|
|
36
48
|
tier: 2,
|
|
37
49
|
skillsDir: SHARED_SKILLS_DIR,
|
|
50
|
+
// Cursor reads both, and additionally loads Claude and Codex directories.
|
|
51
|
+
discovery: Object.freeze(['.agents/skills', '.cursor/skills']),
|
|
52
|
+
docs: 'https://cursor.com/docs/skills',
|
|
53
|
+
invoke: 'type / in Agent chat and pick qa-explore',
|
|
38
54
|
wrapperFormat: null,
|
|
39
55
|
wrapperDir: null,
|
|
40
56
|
detect: (root) =>
|
|
@@ -46,6 +62,12 @@ export const AGENTS = Object.freeze([
|
|
|
46
62
|
name: 'OpenAI Codex CLI',
|
|
47
63
|
tier: 1,
|
|
48
64
|
skillsDir: SHARED_SKILLS_DIR,
|
|
65
|
+
// Codex documents `.agents/skills` exclusively — at the cwd, the parent, and
|
|
66
|
+
// the repository root. There is no `.codex/skills`, so `.codex/` is only ever
|
|
67
|
+
// a detection marker here, never a target.
|
|
68
|
+
discovery: Object.freeze(['.agents/skills']),
|
|
69
|
+
docs: 'https://developers.openai.com/codex/skills',
|
|
70
|
+
invoke: 'type $qa-explore, or /skills to browse them',
|
|
49
71
|
wrapperFormat: null,
|
|
50
72
|
wrapperDir: null,
|
|
51
73
|
detect: (root) =>
|
|
@@ -57,7 +79,12 @@ export const AGENTS = Object.freeze([
|
|
|
57
79
|
name: 'OpenCode',
|
|
58
80
|
tier: 1,
|
|
59
81
|
skillsDir: SHARED_SKILLS_DIR,
|
|
82
|
+
discovery: Object.freeze(['.agents/skills', '.opencode/skills', '.claude/skills']),
|
|
83
|
+
docs: 'https://opencode.ai/docs/skills/',
|
|
84
|
+
invoke: 'type /qa-explore, or describe the task — the agent loads skills itself',
|
|
60
85
|
wrapperFormat: 'command-md',
|
|
86
|
+
// Plural, per https://opencode.ai/docs/commands/ — global is
|
|
87
|
+
// ~/.config/opencode/commands/, per-project .opencode/commands/.
|
|
61
88
|
wrapperDir: '.opencode/commands',
|
|
62
89
|
detect: (root) => fs.existsSync(path.join(root, '.opencode')),
|
|
63
90
|
},
|
|
@@ -66,6 +93,9 @@ export const AGENTS = Object.freeze([
|
|
|
66
93
|
name: 'Gemini CLI',
|
|
67
94
|
tier: 2,
|
|
68
95
|
skillsDir: SHARED_SKILLS_DIR,
|
|
96
|
+
discovery: Object.freeze(['.agents/skills', '.gemini/skills']),
|
|
97
|
+
docs: 'https://google-gemini.github.io/gemini-cli/',
|
|
98
|
+
invoke: 'type /qa-explore (generated command), or describe the task',
|
|
69
99
|
wrapperFormat: 'command-toml',
|
|
70
100
|
wrapperDir: '.gemini/commands',
|
|
71
101
|
detect: (root) => fs.existsSync(path.join(root, '.gemini')),
|
|
@@ -75,6 +105,9 @@ export const AGENTS = Object.freeze([
|
|
|
75
105
|
name: 'GitHub Copilot',
|
|
76
106
|
tier: 2,
|
|
77
107
|
skillsDir: SHARED_SKILLS_DIR,
|
|
108
|
+
discovery: Object.freeze(['.agents/skills', '.github/skills']),
|
|
109
|
+
docs: 'https://docs.github.com/en/copilot',
|
|
110
|
+
invoke: 'type /qa-explore in Copilot Chat (generated prompt file)',
|
|
78
111
|
wrapperFormat: 'prompt-md',
|
|
79
112
|
wrapperDir: '.github/prompts',
|
|
80
113
|
// Detected by a Copilot-specific marker, not by `.github/` — almost every
|
|
@@ -90,6 +123,13 @@ export const AGENTS = Object.freeze([
|
|
|
90
123
|
name: 'Antigravity',
|
|
91
124
|
tier: 2,
|
|
92
125
|
skillsDir: SHARED_SKILLS_DIR,
|
|
126
|
+
// Antigravity defaults to `.agents/skills` at the workspace root, with
|
|
127
|
+
// backward support for `.agent/skills`; global skills live under
|
|
128
|
+
// ~/.gemini/config/skills. So the pack's default install path is already the
|
|
129
|
+
// one it reads — detection below only governs the optional workflow wrappers.
|
|
130
|
+
discovery: Object.freeze(['.agents/skills', '.agent/skills']),
|
|
131
|
+
docs: 'https://antigravity.google/docs/skills',
|
|
132
|
+
invoke: 'describe the task — skills auto-activate; workflows give slash commands',
|
|
93
133
|
wrapperFormat: 'workflow-md',
|
|
94
134
|
wrapperDir: '.agents/workflows',
|
|
95
135
|
// Detected by its own configuration directory, NOT by `.agents/`.
|
|
@@ -106,6 +146,9 @@ export const AGENTS = Object.freeze([
|
|
|
106
146
|
name: 'Kimi (Agent Skills copy)',
|
|
107
147
|
tier: 2,
|
|
108
148
|
skillsDir: SHARED_SKILLS_DIR,
|
|
149
|
+
discovery: Object.freeze(['.agents/skills']),
|
|
150
|
+
docs: 'https://agentskills.io/specification',
|
|
151
|
+
invoke: 'describe the task, or use the product\'s own slash surface',
|
|
109
152
|
wrapperFormat: null,
|
|
110
153
|
wrapperDir: null,
|
|
111
154
|
detect: () => false,
|
|
@@ -119,6 +162,9 @@ export const AGENTS = Object.freeze([
|
|
|
119
162
|
name: 'Unknown agent (shared Agent Skills path)',
|
|
120
163
|
tier: null,
|
|
121
164
|
skillsDir: SHARED_SKILLS_DIR,
|
|
165
|
+
discovery: Object.freeze(['.agents/skills']),
|
|
166
|
+
docs: 'https://agentskills.io/specification',
|
|
167
|
+
invoke: 'describe the task; most hosts also expose skills on /',
|
|
122
168
|
wrapperFormat: null,
|
|
123
169
|
wrapperDir: null,
|
|
124
170
|
detected: false,
|
|
@@ -30,7 +30,7 @@ export async function runDoctor(argv, { log } = {}) {
|
|
|
30
30
|
if (opts.help) {
|
|
31
31
|
logger.result(`Usage: qa doctor [--project <dir>] [--json]
|
|
32
32
|
|
|
33
|
-
Diagnose environment and QA
|
|
33
|
+
Diagnose environment and QA Engineer Pack installation.
|
|
34
34
|
Each failure includes an exact repair command.`);
|
|
35
35
|
return EXIT.OK;
|
|
36
36
|
}
|
|
@@ -90,7 +90,7 @@ Each failure includes an exact repair command.`);
|
|
|
90
90
|
if (validation) {
|
|
91
91
|
for (const c of validation.checks) {
|
|
92
92
|
checklist.push({
|
|
93
|
-
section: 'QA
|
|
93
|
+
section: 'QA Engineer Pack',
|
|
94
94
|
id: c.id,
|
|
95
95
|
ok: c.ok,
|
|
96
96
|
message: c.message,
|
|
@@ -99,14 +99,14 @@ Each failure includes an exact repair command.`);
|
|
|
99
99
|
}
|
|
100
100
|
} else {
|
|
101
101
|
checklist.push({
|
|
102
|
-
section: 'QA
|
|
102
|
+
section: 'QA Engineer Pack',
|
|
103
103
|
id: 'lockfile',
|
|
104
104
|
ok: false,
|
|
105
105
|
message: 'no qa-lock.json',
|
|
106
106
|
hint: 'run: qa install',
|
|
107
107
|
});
|
|
108
108
|
checklist.push({
|
|
109
|
-
section: 'QA
|
|
109
|
+
section: 'QA Engineer Pack',
|
|
110
110
|
id: 'skills',
|
|
111
111
|
ok: false,
|
|
112
112
|
message: 'skills not installed',
|
|
@@ -124,7 +124,7 @@ Each failure includes an exact repair command.`);
|
|
|
124
124
|
const packages = bundlePackagesForSkill(bundledSkill);
|
|
125
125
|
const result = verifyImports({ pythonBin: python.bin, libDir: resolvedLib, packages });
|
|
126
126
|
checklist.push({
|
|
127
|
-
section: 'QA
|
|
127
|
+
section: 'QA Engineer Pack',
|
|
128
128
|
id: 'engine-imports',
|
|
129
129
|
ok: result.ok,
|
|
130
130
|
message: result.ok ? 'bundled engine runs cleanly' : `engine check failed: ${result.stderr}`,
|
|
@@ -160,7 +160,7 @@ Each failure includes an exact repair command.`);
|
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
if (!opts.json) {
|
|
163
|
-
logger.step(`QA
|
|
163
|
+
logger.step(`QA Engineer Pack doctor — ${VERSION}`);
|
|
164
164
|
logger.step(`project ${root}`);
|
|
165
165
|
let section = '';
|
|
166
166
|
for (const item of checklist) {
|
|
@@ -23,6 +23,31 @@ import { INSTALL_STEPS, progressBar } from '../ui/progress.mjs';
|
|
|
23
23
|
import { detectFrameworks } from '../detect/frameworks.mjs';
|
|
24
24
|
import { getFramework } from '../../../../shared/frameworks/registry.mjs';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Say how to reach a skill in the host that was actually detected.
|
|
28
|
+
*
|
|
29
|
+
* The invocation surface differs per host — Codex takes `$qa-explore`, Cursor
|
|
30
|
+
* matches on `/`, OpenCode's agent loads skills itself — and a user who types the
|
|
31
|
+
* wrong one concludes the install failed. The registry records each host's own
|
|
32
|
+
* convention beside the paths it reads, so this prints what will work here rather
|
|
33
|
+
* than a generic "/qa-explore" that is wrong in two of the five hosts.
|
|
34
|
+
*/
|
|
35
|
+
function reportInvocation(agents, logger) {
|
|
36
|
+
const named = agents.filter((a) => a.invoke && a.id !== 'agent-skills');
|
|
37
|
+
if (named.length === 0) {
|
|
38
|
+
const fallback = agents.find((a) => a.invoke);
|
|
39
|
+
if (fallback) {
|
|
40
|
+
logger.info(` → no specific agent detected; ${fallback.invoke}`);
|
|
41
|
+
logger.info(' → the skills are on the standard Agent Skills path, which every');
|
|
42
|
+
logger.info(' supported host reads: Cursor, Codex, OpenCode, Antigravity, Gemini, Copilot');
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const agent of named) {
|
|
47
|
+
logger.info(` → in ${agent.name}: ${agent.invoke}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
/**
|
|
27
52
|
* Say plainly which commands this project can actually use.
|
|
28
53
|
*
|
|
@@ -60,10 +85,11 @@ function reportFrameworkFit(root, logger) {
|
|
|
60
85
|
return;
|
|
61
86
|
}
|
|
62
87
|
|
|
63
|
-
logger.warn('no
|
|
64
|
-
logger.info(' →
|
|
65
|
-
logger.info(' →
|
|
66
|
-
logger.info(' →
|
|
88
|
+
logger.warn('no end-to-end framework detected (Playwright, Selenium, Cypress, WebdriverIO)');
|
|
89
|
+
logger.info(' → start with /qa-generate — it bootstraps a framework when none exists');
|
|
90
|
+
logger.info(' → /qa-run needs one first; it will say so rather than guess');
|
|
91
|
+
logger.info(' → these work today: /qa-review, /qa-api, /qa-audit, /qa-explore, /qa-report');
|
|
92
|
+
logger.info(' → unit tests only (Jest, Vitest, Jasmine, pytest)? That is expected');
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
/**
|
|
@@ -250,6 +276,7 @@ export async function executeInstall({
|
|
|
250
276
|
} else if (!json) {
|
|
251
277
|
logger.ok(`installed ${unique.length} file(s); lockfile ${lockPath(root)}`);
|
|
252
278
|
for (const step of INSTALL_STEPS) logger.ok(step.label);
|
|
279
|
+
reportInvocation(agents, logger);
|
|
253
280
|
reportFrameworkFit(root, logger);
|
|
254
281
|
}
|
|
255
282
|
|
|
@@ -271,7 +298,7 @@ export async function runInstall(argv, { log } = {}) {
|
|
|
271
298
|
if (opts.help) {
|
|
272
299
|
logger.result(`Usage: qa install [--agent <id>]... [--force] [--dry-run] [--yes] [--json] [--project <dir>]
|
|
273
300
|
|
|
274
|
-
Copy QA
|
|
301
|
+
Copy QA Engineer Pack skills into Agent Skills discovery paths
|
|
275
302
|
(.agents/skills/ and .claude/skills/ when applicable), write qa-lock.json,
|
|
276
303
|
and generate thin slash wrappers for agents that need them.`);
|
|
277
304
|
return EXIT.OK;
|
|
@@ -65,7 +65,7 @@ async function guidedFirstRun(ui, { skip }) {
|
|
|
65
65
|
);
|
|
66
66
|
await ui.confirm('Continue when done', { initialValue: true });
|
|
67
67
|
|
|
68
|
-
ui.success('Your QA
|
|
68
|
+
ui.success('Your QA Engineer Pack is fully operational.');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export async function runOnboard(argv, { log } = {}) {
|
|
@@ -90,7 +90,7 @@ Options:
|
|
|
90
90
|
const ui = createUi({ quiet: nonInteractive && !opts.json, json: opts.json });
|
|
91
91
|
|
|
92
92
|
if (!nonInteractive) {
|
|
93
|
-
ui.intro('Welcome to QA
|
|
93
|
+
ui.intro('Welcome to QA Engineer Pack');
|
|
94
94
|
ui.logLine('Scanning your environment...');
|
|
95
95
|
} else if (!opts.json) {
|
|
96
96
|
logger.step('Scanning environment (non-interactive)...');
|
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { CONFIG_FILE } from '../constants.mjs';
|
|
9
9
|
import { QaError, usageError } from './errors.mjs';
|
|
10
|
-
import { validate } from '
|
|
10
|
+
import { validate } from '../../../engine/lib/analysis/contracts.mjs';
|
|
11
11
|
|
|
12
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const SCHEMA_PATH = path.join(here, '..', '..', 'schemas', 'qa.config.schema.json');
|
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { LOCKFILE, PACK_NAME } from '../constants.mjs';
|
|
9
9
|
import { VERSION, SPEC_REVISION } from '../version.mjs';
|
|
10
10
|
import { QaError } from './errors.mjs';
|
|
11
|
-
import { validate } from '
|
|
11
|
+
import { validate } from '../../../engine/lib/analysis/contracts.mjs';
|
|
12
12
|
|
|
13
13
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const SCHEMA_PATH = path.join(here, '..', '..', 'schemas', 'qa-lock.schema.json');
|
|
@@ -18,6 +18,9 @@ export const BUNDLE_MANIFEST = Object.freeze({
|
|
|
18
18
|
'qa-debug': Object.freeze({ packages: ['qa_analysis', 'qa_diagnostics'], modules: ['playwright_analysis'] }),
|
|
19
19
|
'qa-fix': Object.freeze({ packages: ['qa_analysis', 'qa_diagnostics'], modules: [] }),
|
|
20
20
|
'qa-report': Object.freeze({ packages: ['qa_analysis', 'qa_diagnostics'], modules: [] }),
|
|
21
|
+
// qa-explore renders its HTML report with qa_analysis.report_html rather than
|
|
22
|
+
// typing it, so every required finding field reaches the page.
|
|
23
|
+
'qa-explore': Object.freeze({ packages: ['qa_analysis'], modules: [] }),
|
|
21
24
|
'qa-flaky': Object.freeze({ packages: ['qa_analysis', 'qa_diagnostics'], modules: [] }),
|
|
22
25
|
'qa-api': Object.freeze({ packages: ['qa_analysis', 'qa_diagnostics'], modules: [] }),
|
|
23
26
|
'qa-audit': Object.freeze({ packages: ['qa_analysis', 'qa_diagnostics'], modules: [] }),
|
|
@@ -25,7 +25,7 @@ const RENDERERS = {
|
|
|
25
25
|
`# ${GENERATED_MARKER}\n` +
|
|
26
26
|
`description = ${quote(desc)}\n` +
|
|
27
27
|
'prompt = """\n' +
|
|
28
|
-
`Apply the ${name} skill from the QA
|
|
28
|
+
`Apply the ${name} skill from the QA Engineer Pack skills, following its procedure.\n` +
|
|
29
29
|
'Treat the following as the user request:\n\n' +
|
|
30
30
|
'{{args}}\n' +
|
|
31
31
|
'"""\n',
|
|
@@ -39,7 +39,7 @@ const RENDERERS = {
|
|
|
39
39
|
`description: ${quote(desc)}\n` +
|
|
40
40
|
'---\n' +
|
|
41
41
|
`<!-- ${GENERATED_MARKER} -->\n\n` +
|
|
42
|
-
`Apply the \`${name}\` skill from the QA
|
|
42
|
+
`Apply the \`${name}\` skill from the QA Engineer Pack to the request below, following its procedure.\n\n` +
|
|
43
43
|
'$ARGUMENTS\n',
|
|
44
44
|
}),
|
|
45
45
|
|
|
@@ -51,7 +51,7 @@ const RENDERERS = {
|
|
|
51
51
|
`description: ${quote(desc)}\n` +
|
|
52
52
|
'---\n' +
|
|
53
53
|
`<!-- ${GENERATED_MARKER} -->\n\n` +
|
|
54
|
-
`Apply the \`${name}\` skill from the QA
|
|
54
|
+
`Apply the \`${name}\` skill from the QA Engineer Pack to the current request, following its procedure.\n`,
|
|
55
55
|
}),
|
|
56
56
|
|
|
57
57
|
// Antigravity workflow (Markdown). Format is not yet verified against primary
|
|
@@ -64,7 +64,7 @@ const RENDERERS = {
|
|
|
64
64
|
`description: ${quote(desc)}\n` +
|
|
65
65
|
'---\n' +
|
|
66
66
|
`<!-- ${GENERATED_MARKER} -->\n\n` +
|
|
67
|
-
`Apply the \`${name}\` skill from the QA
|
|
67
|
+
`Apply the \`${name}\` skill from the QA Engineer Pack to the request below, following its procedure.\n\n` +
|
|
68
68
|
'$ARGUMENTS\n',
|
|
69
69
|
}),
|
|
70
70
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "urn:qa-pack:config:qa-lock:1",
|
|
4
|
-
"title": "QA
|
|
4
|
+
"title": "QA Engineer Pack install lockfile",
|
|
5
5
|
"description": "qa-lock.json records exactly what was installed and a hash per file, so an install can be verified, updated, or removed without guessing. Written by the installer; not hand-edited.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "urn:qa-pack:config:qa-config:1",
|
|
4
|
-
"title": "QA
|
|
4
|
+
"title": "QA Engineer Pack project configuration",
|
|
5
5
|
"description": "The optional qa.config.json written at a project root. Every field has a documented default; the file exists only to override those defaults. See docs/architecture/ADR-0015-configuration-contract.md.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "The single source of truth for product attribution in human-readable reports. Every renderer reads this file; no branding string is hardcoded anywhere else in the repository, and scripts/check-branding.mjs fails if one appears. Changing the footer means editing this file only.",
|
|
3
|
-
"projectName": "QA
|
|
3
|
+
"projectName": "QA Engineer Pack",
|
|
4
4
|
"tagline": "AI-First Deterministic QA Engineering Platform",
|
|
5
5
|
"author": "Abisheik",
|
|
6
6
|
"website": "https://abisheik.dev",
|
|
@@ -14,6 +14,7 @@ Usage:
|
|
|
14
14
|
python -m qa_analysis.cli classify "<error message>" [--http-status N]
|
|
15
15
|
python -m qa_analysis.cli context [--root DIR] [--path .qa/context.md]
|
|
16
16
|
python -m qa_analysis.cli branding [--format html|markdown|text]
|
|
17
|
+
python -m qa_analysis.cli report-html <result.json> [--out report.html]
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
20
|
import argparse
|
|
@@ -23,6 +24,7 @@ import sys
|
|
|
23
24
|
|
|
24
25
|
from . import junit, har, discovery, diff_guard, redaction, contracts, taxonomy
|
|
25
26
|
from . import branding as branding_module
|
|
27
|
+
from . import report_html as report_html_module
|
|
26
28
|
from . import context as context_module
|
|
27
29
|
from .context import MalformedContext
|
|
28
30
|
from .junit import MalformedArtifact
|
|
@@ -61,6 +63,8 @@ def main(argv=None):
|
|
|
61
63
|
p = sub.add_parser("validate"); p.add_argument("instance"); p.add_argument("schema")
|
|
62
64
|
p = sub.add_parser("classify"); p.add_argument("message"); p.add_argument("--http-status", type=int, default=None)
|
|
63
65
|
p = sub.add_parser("context"); p.add_argument("--root", default="."); p.add_argument("--path", default=None)
|
|
66
|
+
p = sub.add_parser("report-html")
|
|
67
|
+
p.add_argument("path"); p.add_argument("--out"); p.add_argument("--title")
|
|
64
68
|
p = sub.add_parser("branding")
|
|
65
69
|
p.add_argument("--format", default="text", choices=list(branding_module.FORMATS) + ["pdf", "md", "txt"])
|
|
66
70
|
p.add_argument("--metadata", action="store_true", help="emit the branding metadata as JSON instead")
|
|
@@ -110,6 +114,17 @@ def main(argv=None):
|
|
|
110
114
|
_emit(branding_module.metadata())
|
|
111
115
|
else:
|
|
112
116
|
sys.stdout.write(branding_module.footer(args.format))
|
|
117
|
+
elif args.command == "report-html":
|
|
118
|
+
document = report_html_module.render_file(args.path, title=args.title)
|
|
119
|
+
if args.out:
|
|
120
|
+
with open(args.out, "w", encoding="utf-8") as handle:
|
|
121
|
+
handle.write(document)
|
|
122
|
+
_emit({"written": args.out, "bytes": len(document)})
|
|
123
|
+
else:
|
|
124
|
+
sys.stdout.write(document)
|
|
125
|
+
except report_html_module.ReportError as exc:
|
|
126
|
+
_emit({"error": "report-error", "detail": str(exc)})
|
|
127
|
+
return 2
|
|
113
128
|
except branding_module.BrandingError as exc:
|
|
114
129
|
_emit({"error": "branding-error", "detail": str(exc)})
|
|
115
130
|
return 2
|
|
@@ -30,17 +30,27 @@ def parse_har(path, slow_ms=1000):
|
|
|
30
30
|
except (KeyError, TypeError) as exc:
|
|
31
31
|
raise MalformedArtifact(f"not a HAR document at {path}") from exc
|
|
32
32
|
|
|
33
|
+
# The shape has to be checked, not assumed. `log.entries` being a *string*
|
|
34
|
+
# satisfies the subscripting above, and iterating it then yields characters,
|
|
35
|
+
# so the parser died on `'str' object has no attribute 'get'` — an
|
|
36
|
+
# AttributeError, which the CLI does not catch, so the caller got a traceback
|
|
37
|
+
# instead of the documented exit 2. Found by the Node port's parity gate.
|
|
38
|
+
if not isinstance(raw_entries, list):
|
|
39
|
+
raise MalformedArtifact(f"HAR log.entries is not a list at {path}")
|
|
40
|
+
|
|
33
41
|
entries = []
|
|
34
42
|
for item in raw_entries:
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
if not isinstance(item, dict):
|
|
44
|
+
raise MalformedArtifact(f"HAR log.entries contains a non-object at {path}")
|
|
45
|
+
request = item.get("request") or {}
|
|
46
|
+
response = item.get("response") or {}
|
|
37
47
|
status = _int(response.get("status"))
|
|
38
48
|
entry = {
|
|
39
49
|
"method": request.get("method", ""),
|
|
40
50
|
# Redaction strips any credentials embedded in the URL.
|
|
41
51
|
"url": redact_text(request.get("url", "")),
|
|
42
52
|
"status": status,
|
|
43
|
-
"durationMs":
|
|
53
|
+
"durationMs": _millis(item.get("time"), path),
|
|
44
54
|
"requestHeaders": redact_headers(request.get("headers", [])),
|
|
45
55
|
"responseHeaders": redact_headers(response.get("headers", [])),
|
|
46
56
|
}
|
|
@@ -56,3 +66,22 @@ def _int(value, default=0):
|
|
|
56
66
|
return int(value)
|
|
57
67
|
except (TypeError, ValueError):
|
|
58
68
|
return default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _millis(raw, path):
|
|
72
|
+
"""HAR `time` is already milliseconds, rounded to a whole number.
|
|
73
|
+
|
|
74
|
+
A value that is present but not a number raises rather than becoming 0 — the
|
|
75
|
+
same rule as the JUnit parser, and for the same reason: a fabricated duration
|
|
76
|
+
makes a malformed document look like a clean measurement. Before this, the
|
|
77
|
+
bare `float()` escaped as a ValueError and reached the caller as a traceback.
|
|
78
|
+
"""
|
|
79
|
+
if raw is None or raw == "":
|
|
80
|
+
return 0
|
|
81
|
+
try:
|
|
82
|
+
value = float(raw)
|
|
83
|
+
except (TypeError, ValueError):
|
|
84
|
+
raise MalformedArtifact(f"entry time={raw!r} is not a number at {path}") from None
|
|
85
|
+
if value != value or value in (float("inf"), float("-inf")):
|
|
86
|
+
raise MalformedArtifact(f"entry time={raw!r} is not a finite number at {path}")
|
|
87
|
+
return int(round(value))
|