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.
Files changed (52) hide show
  1. package/COMPATIBILITY.md +25 -15
  2. package/LICENSE +1 -1
  3. package/README.md +22 -4
  4. package/package.json +4 -3
  5. package/packages/installer/README.md +1 -1
  6. package/packages/installer/lib/agents/registry.mjs +46 -0
  7. package/packages/installer/lib/commands/doctor.mjs +6 -6
  8. package/packages/installer/lib/commands/install.mjs +32 -5
  9. package/packages/installer/lib/commands/onboard.mjs +2 -2
  10. package/packages/installer/lib/core/config.mjs +1 -1
  11. package/packages/installer/lib/core/lockfile.mjs +1 -1
  12. package/packages/installer/lib/core/manifest.mjs +3 -0
  13. package/packages/installer/lib/core/wrappers.mjs +4 -4
  14. package/packages/installer/lib/ui/theme.mjs +1 -1
  15. package/packages/installer/package.json +1 -1
  16. package/packages/installer/schemas/qa-lock.schema.json +1 -1
  17. package/packages/installer/schemas/qa.config.schema.json +1 -1
  18. package/shared/analysis/lib/qa_analysis/branding.json +1 -1
  19. package/shared/analysis/lib/qa_analysis/cli.py +15 -0
  20. package/shared/analysis/lib/qa_analysis/har.py +32 -3
  21. package/shared/analysis/lib/qa_analysis/junit.py +25 -1
  22. package/shared/analysis/lib/qa_analysis/redaction.py +10 -2
  23. package/shared/analysis/lib/qa_analysis/report_html.py +781 -0
  24. package/skills/qa/README.md +1 -1
  25. package/skills/qa/SKILL.md +1 -1
  26. package/skills/qa-api/references/deterministic-tooling.md +2 -0
  27. package/skills/qa-api/references/evidence-and-reporting.md +29 -4
  28. package/skills/qa-audit/references/deterministic-tooling.md +2 -0
  29. package/skills/qa-audit/references/evidence-and-reporting.md +29 -4
  30. package/skills/qa-debug/references/deterministic-tooling.md +2 -0
  31. package/skills/qa-debug/references/evidence-and-reporting.md +29 -4
  32. package/skills/qa-example/SKILL.md +1 -1
  33. package/skills/qa-example/contracts/self-check-report.schema.json +1 -1
  34. package/skills/qa-explore/SKILL.md +22 -5
  35. package/skills/qa-explore/contracts/explore-result.schema.json +26 -2
  36. package/skills/qa-explore/references/deterministic-tooling.md +130 -0
  37. package/skills/qa-explore/references/evidence-and-reporting.md +29 -4
  38. package/skills/qa-explore/references/report-pipeline.md +89 -8
  39. package/skills/qa-fix/references/deterministic-tooling.md +2 -0
  40. package/skills/qa-fix/references/evidence-and-reporting.md +29 -4
  41. package/skills/qa-flaky/references/deterministic-tooling.md +2 -0
  42. package/skills/qa-flaky/references/evidence-and-reporting.md +29 -4
  43. package/skills/qa-generate/references/evidence-and-reporting.md +29 -4
  44. package/skills/qa-init/references/deterministic-tooling.md +2 -0
  45. package/skills/qa-init/references/evidence-and-reporting.md +29 -4
  46. package/skills/qa-report/SKILL.md +3 -2
  47. package/skills/qa-report/references/deterministic-tooling.md +2 -0
  48. package/skills/qa-report/references/evidence-and-reporting.md +29 -4
  49. package/skills/qa-review/references/evidence-and-reporting.md +29 -4
  50. package/skills/qa-run/references/deterministic-tooling.md +2 -0
  51. package/skills/qa-run/references/evidence-and-reporting.md +29 -4
  52. 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
- No versions have been released yet. Beginning with the first tagged release, this section will carry a matrix of pack versions against tested agent versions and the Agent Skills specification revision each release was validated against.
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
- Verified against official documentation as of July 2026. Entries marked *unverified* rely on secondary sources and will be confirmed before the relevant integration ships.
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
- | Agent | Native `SKILL.md` support | Skill discovery path the pack targets | Invocation surface | Planned tier |
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
- Two facts shape the integration design:
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
- 1. Six of the seven primary agents read `.agents/skills/` directly; Claude Code additionally requires `.claude/skills/`. A single canonical skill set therefore covers every target with copies, not conversions. Additional hosts (Kimi and peers) use the same copy path see [docs/installation/other-agents.md](docs/installation/other-agents.md).
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 Automation Pack contributors
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 Automation Pack
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
- ## All twelve commands
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
- Type these in your AI assistant's chat, not the terminal.
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 Automation Pack contributors. Use it commercially, fork it, modify it.
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.0",
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 Automation 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.**
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 Automation Pack installation.
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 Automation Pack',
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 Automation Pack',
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 Automation Pack',
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 Automation Pack',
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 Automation Pack doctor — ${VERSION}`);
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 supported end-to-end framework detected (Playwright, Selenium, Cypress, WebdriverIO)');
64
- logger.info(' → /qa-run and /qa-generate need one; they will tell you so rather than guess');
65
- logger.info(' → these work without one: /qa-review, /qa-api, /qa-report, /qa-audit, /qa-explore');
66
- logger.info(' → unit tests only (Jest, Vitest, pytest)? That is expected — see the README');
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 Automation Pack skills into Agent Skills discovery paths
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 Automation Pack is fully operational.');
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 Automation Pack');
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 './schema-validate.mjs';
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 './schema-validate.mjs';
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 Automation Pack skills, following its procedure.\n` +
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 Automation Pack to the request below, following its procedure.\n\n` +
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 Automation Pack to the current request, following its procedure.\n`,
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 Automation Pack to the request below, following its procedure.\n\n` +
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
  };
@@ -14,7 +14,7 @@ export function createUi({ quiet = false, json = false } = {}) {
14
14
 
15
15
  return {
16
16
  log,
17
- intro(message = 'Welcome to QA Automation Pack') {
17
+ intro(message = 'Welcome to QA Engineer Pack') {
18
18
  if (quiet || json) return;
19
19
  p.intro(message);
20
20
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qa-engineer/installer",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "The qa-engineer installer: agent detection, copy-based install, lockfile integrity, and wrapper generation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 Automation Pack install lockfile",
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 Automation Pack project configuration",
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 Automation Pack",
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
- request = item.get("request", {})
36
- response = item.get("response", {})
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": int(round(float(item.get("time", 0) or 0))),
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))