qa-engineer 0.9.0

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 (232) hide show
  1. package/COMPATIBILITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +508 -0
  4. package/package.json +90 -0
  5. package/packages/installer/README.md +46 -0
  6. package/packages/installer/bin/qa.mjs +136 -0
  7. package/packages/installer/lib/agents/registry.mjs +209 -0
  8. package/packages/installer/lib/cli/commands.mjs +29 -0
  9. package/packages/installer/lib/cli/flags.mjs +64 -0
  10. package/packages/installer/lib/commands/doctor.mjs +190 -0
  11. package/packages/installer/lib/commands/install.mjs +291 -0
  12. package/packages/installer/lib/commands/onboard.mjs +163 -0
  13. package/packages/installer/lib/commands/repair.mjs +68 -0
  14. package/packages/installer/lib/commands/self-test.mjs +45 -0
  15. package/packages/installer/lib/commands/uninstall.mjs +167 -0
  16. package/packages/installer/lib/commands/update.mjs +69 -0
  17. package/packages/installer/lib/commands/verify.mjs +62 -0
  18. package/packages/installer/lib/constants.mjs +34 -0
  19. package/packages/installer/lib/core/bundle.mjs +124 -0
  20. package/packages/installer/lib/core/config.mjs +73 -0
  21. package/packages/installer/lib/core/conflict.mjs +29 -0
  22. package/packages/installer/lib/core/errors.mjs +29 -0
  23. package/packages/installer/lib/core/fs-safe.mjs +236 -0
  24. package/packages/installer/lib/core/hash.mjs +19 -0
  25. package/packages/installer/lib/core/lockfile.mjs +75 -0
  26. package/packages/installer/lib/core/logger.mjs +46 -0
  27. package/packages/installer/lib/core/manifest.mjs +89 -0
  28. package/packages/installer/lib/core/paths.mjs +74 -0
  29. package/packages/installer/lib/core/schema-validate.mjs +142 -0
  30. package/packages/installer/lib/core/skill-meta.mjs +75 -0
  31. package/packages/installer/lib/core/validate-install.mjs +152 -0
  32. package/packages/installer/lib/core/wrappers.mjs +91 -0
  33. package/packages/installer/lib/detect/environment.mjs +54 -0
  34. package/packages/installer/lib/detect/frameworks.mjs +94 -0
  35. package/packages/installer/lib/detect/project.mjs +126 -0
  36. package/packages/installer/lib/detect/recommend.mjs +85 -0
  37. package/packages/installer/lib/detect/scan.mjs +48 -0
  38. package/packages/installer/lib/ui/progress.mjs +32 -0
  39. package/packages/installer/lib/ui/theme.mjs +81 -0
  40. package/packages/installer/lib/version.mjs +47 -0
  41. package/packages/installer/package.json +28 -0
  42. package/packages/installer/schemas/qa-lock.schema.json +89 -0
  43. package/packages/installer/schemas/qa.config.schema.json +91 -0
  44. package/shared/analysis/lib/qa_analysis/__init__.py +11 -0
  45. package/shared/analysis/lib/qa_analysis/branding.json +9 -0
  46. package/shared/analysis/lib/qa_analysis/branding.py +175 -0
  47. package/shared/analysis/lib/qa_analysis/cli.py +129 -0
  48. package/shared/analysis/lib/qa_analysis/context.py +233 -0
  49. package/shared/analysis/lib/qa_analysis/contracts.py +158 -0
  50. package/shared/analysis/lib/qa_analysis/diff_guard.py +327 -0
  51. package/shared/analysis/lib/qa_analysis/discovery.py +113 -0
  52. package/shared/analysis/lib/qa_analysis/evidence.py +128 -0
  53. package/shared/analysis/lib/qa_analysis/har.py +58 -0
  54. package/shared/analysis/lib/qa_analysis/junit.py +80 -0
  55. package/shared/analysis/lib/qa_analysis/redaction.py +99 -0
  56. package/shared/analysis/lib/qa_analysis/taxonomy.py +98 -0
  57. package/shared/analysis/schemas/context.schema.json +82 -0
  58. package/shared/diagnostics/lib/qa_diagnostics/__init__.py +13 -0
  59. package/shared/diagnostics/lib/qa_diagnostics/cli.py +124 -0
  60. package/shared/diagnostics/lib/qa_diagnostics/engine.py +143 -0
  61. package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +75 -0
  62. package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +101 -0
  63. package/shared/diagnostics/lib/qa_diagnostics/repair.py +72 -0
  64. package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +89 -0
  65. package/shared/diagnostics/lib/qa_diagnostics/timeline.py +71 -0
  66. package/shared/diagnostics/schemas/internal/analysis-result.schema.json +28 -0
  67. package/shared/diagnostics/schemas/internal/diagnosis.schema.json +55 -0
  68. package/shared/diagnostics/schemas/internal/execution-result-min.schema.json +34 -0
  69. package/shared/frameworks/cypress/lib/cypress_analysis.py +27 -0
  70. package/shared/frameworks/playwright/lib/playwright_analysis.py +167 -0
  71. package/shared/frameworks/registry.json +150 -0
  72. package/shared/frameworks/registry.mjs +37 -0
  73. package/shared/frameworks/registry.schema.json +74 -0
  74. package/shared/frameworks/selenium/lib/selenium_analysis.py +28 -0
  75. package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +24 -0
  76. package/shared/tooling/qa_tool.py +127 -0
  77. package/skills/README.md +31 -0
  78. package/skills/qa/README.md +19 -0
  79. package/skills/qa/SKILL.md +52 -0
  80. package/skills/qa/examples/routing.md +86 -0
  81. package/skills/qa/references/routing-map.md +33 -0
  82. package/skills/qa-api/README.md +19 -0
  83. package/skills/qa-api/SKILL.md +68 -0
  84. package/skills/qa-api/contracts/api-result.schema.json +51 -0
  85. package/skills/qa-api/examples/graphql-review.md +43 -0
  86. package/skills/qa-api/references/authentication.md +45 -0
  87. package/skills/qa-api/references/deterministic-tooling.md +128 -0
  88. package/skills/qa-api/references/evidence-and-reporting.md +66 -0
  89. package/skills/qa-api/references/graphql.md +44 -0
  90. package/skills/qa-api/references/rest.md +45 -0
  91. package/skills/qa-api/references/websocket.md +44 -0
  92. package/skills/qa-audit/README.md +19 -0
  93. package/skills/qa-audit/SKILL.md +70 -0
  94. package/skills/qa-audit/contracts/audit-result.schema.json +63 -0
  95. package/skills/qa-audit/examples/accessibility-audit.md +46 -0
  96. package/skills/qa-audit/references/accessibility.md +44 -0
  97. package/skills/qa-audit/references/deterministic-tooling.md +128 -0
  98. package/skills/qa-audit/references/evidence-and-reporting.md +66 -0
  99. package/skills/qa-audit/references/performance.md +44 -0
  100. package/skills/qa-audit/references/security.md +43 -0
  101. package/skills/qa-audit/references/visual-testing.md +44 -0
  102. package/skills/qa-debug/README.md +19 -0
  103. package/skills/qa-debug/SKILL.md +76 -0
  104. package/skills/qa-debug/contracts/debug-result.schema.json +121 -0
  105. package/skills/qa-debug/examples/failed-login.md +61 -0
  106. package/skills/qa-debug/examples/locator-break.md +59 -0
  107. package/skills/qa-debug/examples/network-timeout.md +61 -0
  108. package/skills/qa-debug/examples/successful-debug.md +62 -0
  109. package/skills/qa-debug/references/confidence-model.md +26 -0
  110. package/skills/qa-debug/references/deterministic-tooling.md +128 -0
  111. package/skills/qa-debug/references/diagnostic-engine.md +36 -0
  112. package/skills/qa-debug/references/evidence-and-reporting.md +66 -0
  113. package/skills/qa-debug/references/evidence-model.md +41 -0
  114. package/skills/qa-debug/references/failure-taxonomy.md +44 -0
  115. package/skills/qa-debug/references/finding-prioritization.md +43 -0
  116. package/skills/qa-debug/references/investigation-workflow.md +48 -0
  117. package/skills/qa-debug/references/recommendation-ranking.md +34 -0
  118. package/skills/qa-debug/references/root-cause-analysis.md +49 -0
  119. package/skills/qa-debug/references/timeline-builder.md +37 -0
  120. package/skills/qa-example/README.md +19 -0
  121. package/skills/qa-example/SKILL.md +60 -0
  122. package/skills/qa-example/contracts/self-check-report.schema.json +65 -0
  123. package/skills/qa-example/examples/self-check.md +53 -0
  124. package/skills/qa-example/references/example-domain.md +28 -0
  125. package/skills/qa-example/references/skill-format-notes.md +26 -0
  126. package/skills/qa-explore/README.md +22 -0
  127. package/skills/qa-explore/SKILL.md +87 -0
  128. package/skills/qa-explore/contracts/explore-result.schema.json +249 -0
  129. package/skills/qa-explore/examples/attached-test-cases.md +70 -0
  130. package/skills/qa-explore/examples/screenshot-proof-finding.md +50 -0
  131. package/skills/qa-explore/examples/url-smoke-explore.md +80 -0
  132. package/skills/qa-explore/references/accessibility.md +44 -0
  133. package/skills/qa-explore/references/api-replay.md +48 -0
  134. package/skills/qa-explore/references/browser-adapters.md +51 -0
  135. package/skills/qa-explore/references/evidence-and-reporting.md +66 -0
  136. package/skills/qa-explore/references/evidence-capture.md +54 -0
  137. package/skills/qa-explore/references/exploratory-qa.md +52 -0
  138. package/skills/qa-explore/references/finding-taxonomy.md +47 -0
  139. package/skills/qa-explore/references/performance.md +44 -0
  140. package/skills/qa-explore/references/pipeline.md +74 -0
  141. package/skills/qa-explore/references/report-pipeline.md +69 -0
  142. package/skills/qa-explore/references/security.md +43 -0
  143. package/skills/qa-explore/references/test-case-intake.md +44 -0
  144. package/skills/qa-fix/README.md +19 -0
  145. package/skills/qa-fix/SKILL.md +70 -0
  146. package/skills/qa-fix/contracts/fix-result.schema.json +132 -0
  147. package/skills/qa-fix/examples/repair-plan.md +56 -0
  148. package/skills/qa-fix/references/deterministic-tooling.md +128 -0
  149. package/skills/qa-fix/references/diagnostic-engine.md +36 -0
  150. package/skills/qa-fix/references/evidence-and-reporting.md +66 -0
  151. package/skills/qa-fix/references/repair-strategy.md +48 -0
  152. package/skills/qa-fix/references/root-cause-analysis.md +49 -0
  153. package/skills/qa-fix/references/suite-extension.md +73 -0
  154. package/skills/qa-flaky/README.md +19 -0
  155. package/skills/qa-flaky/SKILL.md +67 -0
  156. package/skills/qa-flaky/contracts/flaky-result.schema.json +62 -0
  157. package/skills/qa-flaky/examples/flaky-locator.md +46 -0
  158. package/skills/qa-flaky/references/deterministic-tooling.md +128 -0
  159. package/skills/qa-flaky/references/evidence-and-reporting.md +66 -0
  160. package/skills/qa-flaky/references/flakiness.md +48 -0
  161. package/skills/qa-flaky/references/retry.md +44 -0
  162. package/skills/qa-flaky/references/waiting-strategies.md +44 -0
  163. package/skills/qa-generate/README.md +42 -0
  164. package/skills/qa-generate/SKILL.md +83 -0
  165. package/skills/qa-generate/contracts/generation-result.schema.json +124 -0
  166. package/skills/qa-generate/examples/bootstrap-new-framework.md +24 -0
  167. package/skills/qa-generate/examples/extend-existing-suite.md +26 -0
  168. package/skills/qa-generate/references/code-style.md +29 -0
  169. package/skills/qa-generate/references/evidence-and-reporting.md +66 -0
  170. package/skills/qa-generate/references/framework-selection.md +88 -0
  171. package/skills/qa-generate/references/generation-strategy.md +48 -0
  172. package/skills/qa-generate/references/naming-conventions.md +27 -0
  173. package/skills/qa-generate/references/playwright-conventions.md +23 -0
  174. package/skills/qa-generate/references/playwright-generation.md +41 -0
  175. package/skills/qa-generate/references/playwright-project-discovery.md +42 -0
  176. package/skills/qa-generate/references/project-bootstrap.md +54 -0
  177. package/skills/qa-generate/references/repository-analysis.md +43 -0
  178. package/skills/qa-generate/references/suite-extension.md +73 -0
  179. package/skills/qa-generate/references/template-selection.md +48 -0
  180. package/skills/qa-generate/templates/playwright/api.ts +36 -0
  181. package/skills/qa-generate/templates/playwright/base-page.ts +14 -0
  182. package/skills/qa-generate/templates/playwright/data.ts +25 -0
  183. package/skills/qa-generate/templates/playwright/env.example +16 -0
  184. package/skills/qa-generate/templates/playwright/example.spec.ts +14 -0
  185. package/skills/qa-generate/templates/playwright/fixtures.ts +28 -0
  186. package/skills/qa-generate/templates/playwright/framework-readme.md +39 -0
  187. package/skills/qa-generate/templates/playwright/login.page.ts +26 -0
  188. package/skills/qa-generate/templates/playwright/playwright.config.ts +28 -0
  189. package/skills/qa-generate/templates/playwright/utils.ts +18 -0
  190. package/skills/qa-init/README.md +20 -0
  191. package/skills/qa-init/SKILL.md +72 -0
  192. package/skills/qa-init/examples/initialize-a-repo.md +91 -0
  193. package/skills/qa-init/references/detection-guide.md +76 -0
  194. package/skills/qa-init/references/deterministic-tooling.md +128 -0
  195. package/skills/qa-init/references/evidence-and-reporting.md +66 -0
  196. package/skills/qa-init/templates/context.md +68 -0
  197. package/skills/qa-report/README.md +19 -0
  198. package/skills/qa-report/SKILL.md +72 -0
  199. package/skills/qa-report/contracts/report-result.schema.json +186 -0
  200. package/skills/qa-report/examples/release-report.md +68 -0
  201. package/skills/qa-report/references/deterministic-tooling.md +128 -0
  202. package/skills/qa-report/references/diagnostic-engine.md +36 -0
  203. package/skills/qa-report/references/evidence-and-reporting.md +66 -0
  204. package/skills/qa-report/references/finding-prioritization.md +43 -0
  205. package/skills/qa-report/references/recommendation-ranking.md +34 -0
  206. package/skills/qa-report/references/report-aggregation.md +44 -0
  207. package/skills/qa-review/README.md +19 -0
  208. package/skills/qa-review/SKILL.md +58 -0
  209. package/skills/qa-review/contracts/review-result.schema.json +59 -0
  210. package/skills/qa-review/examples/suite-review.md +51 -0
  211. package/skills/qa-review/references/anti-patterns.md +49 -0
  212. package/skills/qa-review/references/assertion-patterns.md +45 -0
  213. package/skills/qa-review/references/evidence-and-reporting.md +66 -0
  214. package/skills/qa-review/references/fixtures.md +45 -0
  215. package/skills/qa-review/references/page-objects.md +45 -0
  216. package/skills/qa-run/README.md +22 -0
  217. package/skills/qa-run/SKILL.md +92 -0
  218. package/skills/qa-run/contracts/execution-plan.schema.json +132 -0
  219. package/skills/qa-run/contracts/execution-result.schema.json +204 -0
  220. package/skills/qa-run/examples/execute-playwright.md +89 -0
  221. package/skills/qa-run/examples/plan-a-run.md +83 -0
  222. package/skills/qa-run/references/artifact-collector.md +40 -0
  223. package/skills/qa-run/references/browser-launch.md +39 -0
  224. package/skills/qa-run/references/command-builder.md +39 -0
  225. package/skills/qa-run/references/deterministic-tooling.md +128 -0
  226. package/skills/qa-run/references/environment-detection.md +37 -0
  227. package/skills/qa-run/references/evidence-and-reporting.md +66 -0
  228. package/skills/qa-run/references/execution-strategy.md +54 -0
  229. package/skills/qa-run/references/playwright-artifacts.md +29 -0
  230. package/skills/qa-run/references/playwright-execution.md +48 -0
  231. package/skills/qa-run/references/playwright-project-discovery.md +42 -0
  232. package/skills/qa-run/references/report-normalization.md +49 -0
@@ -0,0 +1,68 @@
1
+ // `qa repair` — fix common install problems by reinstalling pack-owned files.
2
+
3
+ import { EXIT, LOCKFILE } from '../constants.mjs';
4
+ import { resolveProjectRoot } from '../core/paths.mjs';
5
+ import { readLock } from '../core/lockfile.mjs';
6
+ import { parseCommonFlags } from '../cli/flags.mjs';
7
+ import { executeInstall } from './install.mjs';
8
+ import { validateInstall } from '../core/validate-install.mjs';
9
+ import { createLogger } from '../core/logger.mjs';
10
+
11
+ export async function runRepair(argv, { log } = {}) {
12
+ const opts = parseCommonFlags(argv);
13
+ const logger = log ?? createLogger();
14
+ if (opts.help) {
15
+ logger.result(`Usage: qa repair [--project <dir>] [--json] [--dry-run]
16
+
17
+ Repair a broken or drifted installation:
18
+ - missing lockfile → fresh install
19
+ - drifted / missing pack files → reinstall with --force
20
+ - then re-validate`);
21
+ return EXIT.OK;
22
+ }
23
+
24
+ const root = resolveProjectRoot(opts.project ?? process.cwd());
25
+ const prior = readLock(root);
26
+ const before = validateInstall(root);
27
+
28
+ if (!opts.json) {
29
+ logger.step(`repairing ${root}`);
30
+ if (!prior) logger.warn(`no ${LOCKFILE} — performing fresh install`);
31
+ else {
32
+ const bad = before.checks.filter((c) => !c.ok);
33
+ if (bad.length === 0) logger.ok('installation already healthy — refreshing anyway');
34
+ else for (const c of bad) logger.warn(`${c.id}: ${c.message}`);
35
+ }
36
+ }
37
+
38
+ const agentIds = opts.agents.length
39
+ ? opts.agents
40
+ : (prior?.agents ?? []).map((a) => a.id).filter(Boolean);
41
+
42
+ const result = await executeInstall({
43
+ projectRoot: root,
44
+ agentIds,
45
+ force: true,
46
+ dryRun: opts.dryRun,
47
+ json: opts.json,
48
+ log: logger,
49
+ });
50
+
51
+ const after = opts.dryRun ? before : validateInstall(root);
52
+
53
+ if (!opts.json) {
54
+ if (after.ok) logger.ok('repair complete — installation validated');
55
+ else logger.error('repair finished but validation still failing — run: qa doctor');
56
+ }
57
+
58
+ logger.result({
59
+ ok: after.ok,
60
+ repaired: true,
61
+ dryRun: opts.dryRun,
62
+ before: before.checks.filter((c) => !c.ok).map((c) => c.id),
63
+ after: after.checks,
64
+ install: result,
65
+ });
66
+
67
+ return after.ok || opts.dryRun ? EXIT.OK : EXIT.VERIFY;
68
+ }
@@ -0,0 +1,45 @@
1
+ // `qa self-test` — verify an installation is operational.
2
+
3
+ import { EXIT } from '../constants.mjs';
4
+ import { resolveProjectRoot } from '../core/paths.mjs';
5
+ import { parseCommonFlags } from '../cli/flags.mjs';
6
+ import { validateInstall } from '../core/validate-install.mjs';
7
+ import { createLogger } from '../core/logger.mjs';
8
+
9
+ export async function runSelfTest(argv, { log } = {}) {
10
+ const opts = parseCommonFlags(argv);
11
+ const logger = log ?? createLogger();
12
+ if (opts.help) {
13
+ logger.result(`Usage: qa self-test [--project <dir>] [--json]
14
+
15
+ Verify skills, lockfile integrity, bundled engine, contracts, and environment.
16
+ Prints PASS/FAIL per check. Does not invoke live AI agents.`);
17
+ return EXIT.OK;
18
+ }
19
+
20
+ const root = resolveProjectRoot(opts.project ?? process.cwd());
21
+ const { ok, checks } = validateInstall(root);
22
+
23
+ if (!opts.json) {
24
+ logger.step(`self-test — ${root}`);
25
+ for (const check of checks) {
26
+ const tag = check.ok ? 'PASS' : check.hard ? 'FAIL' : 'WARN';
27
+ const line = `[${tag}] ${check.id}: ${check.message}`;
28
+ if (check.ok) logger.ok(line);
29
+ else if (check.hard) logger.error(line);
30
+ else logger.warn(line);
31
+ if (!check.ok && check.hint) logger.info(` → ${check.hint}`);
32
+ }
33
+ if (ok) logger.ok('self-test PASSED');
34
+ else logger.error('self-test FAILED');
35
+ }
36
+
37
+ logger.result({
38
+ ok,
39
+ projectRoot: root,
40
+ checks,
41
+ note: 'Live skill activation is verified in your AI assistant (see guided first-run).',
42
+ });
43
+
44
+ return ok ? EXIT.OK : EXIT.VERIFY;
45
+ }
@@ -0,0 +1,167 @@
1
+ // `qa uninstall` — remove exactly what was installed, and nothing else.
2
+ //
3
+ // The lockfile is the manifest: every path it lists was written by the installer
4
+ // with a recorded hash, so uninstall knows precisely what it owns. Anything not
5
+ // in the lockfile is the user's and is never touched.
6
+ //
7
+ // Removal runs through the same Transaction as install, update, and repair, so
8
+ // it inherits the same guarantees: every deleted file is backed up first, and a
9
+ // failure part-way through restores the project to its prior state rather than
10
+ // leaving a half-removed install.
11
+ //
12
+ // A file that drifted from its recorded hash may carry local edits. Deleting it
13
+ // silently would destroy work, so drift stops the uninstall and names the files;
14
+ // `--force` proceeds (still with backups).
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { EXIT, LOCKFILE, BACKUP_DIR } from '../constants.mjs';
19
+ import { resolveProjectRoot } from '../core/paths.mjs';
20
+ import { readLock, lockPath } from '../core/lockfile.mjs';
21
+ import { hashFile } from '../core/hash.mjs';
22
+ import { Transaction, pruneEmptyDirs } from '../core/fs-safe.mjs';
23
+ import { conflictError, verifyError } from '../core/errors.mjs';
24
+ import { createLogger } from '../core/logger.mjs';
25
+ import { parseCommonFlags } from '../cli/flags.mjs';
26
+
27
+ /** Every __pycache__ directory at or beneath `dir` (empty list if dir is gone). */
28
+ function findPycache(dir) {
29
+ const found = [];
30
+ if (!fs.existsSync(dir)) return found;
31
+ const walk = (current) => {
32
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
33
+ if (!entry.isDirectory()) continue;
34
+ const full = path.join(current, entry.name);
35
+ if (entry.name === '__pycache__') found.push(full);
36
+ else walk(full);
37
+ }
38
+ };
39
+ walk(dir);
40
+ return found;
41
+ }
42
+
43
+ export async function runUninstall(argv, { log } = {}) {
44
+ const opts = parseCommonFlags(argv);
45
+ const logger = log ?? createLogger();
46
+ if (opts.help) {
47
+ logger.result(`Usage: qa uninstall [--project <dir>] [--json] [--dry-run] [--force]
48
+
49
+ Remove every file listed in ${LOCKFILE}, then the lockfile itself.
50
+ Only pack-owned files are removed; anything else is left alone.
51
+
52
+ --dry-run list what would be removed, write nothing
53
+ --force remove files that drifted from their recorded hash`);
54
+ return EXIT.OK;
55
+ }
56
+
57
+ const root = resolveProjectRoot(opts.project ?? process.cwd());
58
+ const lock = readLock(root);
59
+ if (!lock) {
60
+ if (!opts.json) {
61
+ logger.error(`FAIL no ${LOCKFILE} in ${root}`);
62
+ logger.info(' → nothing to uninstall');
63
+ }
64
+ throw verifyError(`no ${LOCKFILE} in ${root}`, 'nothing to uninstall');
65
+ }
66
+
67
+ const present = [];
68
+ const missing = [];
69
+ const drifted = [];
70
+ for (const entry of lock.files) {
71
+ const abs = path.join(root, entry.path);
72
+ if (!fs.existsSync(abs)) {
73
+ missing.push(entry.path);
74
+ continue;
75
+ }
76
+ if (hashFile(abs) !== entry.sha256) drifted.push(entry.path);
77
+ present.push(entry.path);
78
+ }
79
+
80
+ if (drifted.length > 0 && !opts.force) {
81
+ if (!opts.json) {
82
+ logger.error(`FAIL ${drifted.length} installed file(s) have local changes`);
83
+ for (const p of drifted.slice(0, 20)) logger.info(` ${p}`);
84
+ if (drifted.length > 20) logger.info(` … and ${drifted.length - 20} more`);
85
+ logger.info(' → keep them: back them up, then re-run');
86
+ logger.info(' → discard them: qa uninstall --force');
87
+ }
88
+ throw conflictError(
89
+ `${drifted.length} installed file(s) drifted from ${LOCKFILE}`,
90
+ 'run: qa uninstall --force to remove them anyway',
91
+ );
92
+ }
93
+
94
+ if (!opts.json) {
95
+ logger.step(`project: ${root}`);
96
+ logger.step(`removing: ${present.length} file(s)${missing.length ? `, ${missing.length} already gone` : ''}`);
97
+ }
98
+
99
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
100
+ const tx = new Transaction(root, path.join(root, BACKUP_DIR, stamp), { dryRun: opts.dryRun });
101
+ for (const rel of present) tx.delete(rel);
102
+ if (fs.existsSync(lockPath(root))) tx.delete(LOCKFILE);
103
+ const summary = tx.commit();
104
+
105
+ // Byproducts of the pack's own bundled code: Python writes __pycache__ next to
106
+ // the modules it imports, so those directories exist because of us and are not
107
+ // in the lockfile. They are regenerable bytecode, so they are removed without
108
+ // a backup — but only under directories that held pack-owned bundle files,
109
+ // never anywhere else in the project.
110
+ let removedByproducts = 0;
111
+ if (!opts.dryRun) {
112
+ const bundleDirs = new Set();
113
+ for (const rel of lock.files) {
114
+ const marker = rel.path.indexOf('scripts/lib/');
115
+ if (marker !== -1) {
116
+ bundleDirs.add(path.join(root, rel.path.slice(0, marker + 'scripts/lib'.length)));
117
+ }
118
+ }
119
+ for (const bundleDir of bundleDirs) {
120
+ for (const cache of findPycache(bundleDir)) {
121
+ fs.rmSync(cache, { recursive: true, force: true });
122
+ removedByproducts += 1;
123
+ }
124
+ }
125
+ }
126
+
127
+ // Leave no empty skeleton behind: prune directories the pack created, deepest
128
+ // first, stopping at anything that still holds a file the user owns.
129
+ let prunedDirs = 0;
130
+ if (!opts.dryRun) {
131
+ const dirs = new Set();
132
+ for (const rel of [...present, ...missing]) {
133
+ let dir = path.dirname(path.join(root, rel));
134
+ // Walk up to (but never past) the project root.
135
+ while (dir.startsWith(root) && dir !== root) {
136
+ dirs.add(dir);
137
+ dir = path.dirname(dir);
138
+ }
139
+ }
140
+ const before = [...dirs].filter((d) => fs.existsSync(d)).length;
141
+ pruneEmptyDirs([...dirs]);
142
+ prunedDirs = before - [...dirs].filter((d) => fs.existsSync(d)).length;
143
+ }
144
+
145
+ if (!opts.json) {
146
+ if (opts.dryRun) logger.ok(`dry run: would remove ${present.length + 1} file(s)`);
147
+ else {
148
+ logger.ok(`uninstalled ${summary.deleted} file(s); removed ${LOCKFILE}`);
149
+ if (prunedDirs > 0) logger.step(`pruned ${prunedDirs} empty directory(ies)`);
150
+ if (summary.backupDir) logger.step(`backup: ${path.relative(root, summary.backupDir)}`);
151
+ }
152
+ }
153
+
154
+ logger.result({
155
+ ok: true,
156
+ uninstalled: true,
157
+ dryRun: Boolean(opts.dryRun),
158
+ removed: opts.dryRun ? present.length + 1 : summary.deleted,
159
+ alreadyMissing: missing.length,
160
+ driftedRemoved: opts.force ? drifted.length : 0,
161
+ removedByproducts,
162
+ prunedDirectories: prunedDirs,
163
+ backupDir: summary.backupDir ?? null,
164
+ });
165
+
166
+ return EXIT.OK;
167
+ }
@@ -0,0 +1,69 @@
1
+ // `qa update` — refresh an install from the current pack source.
2
+
3
+ import { EXIT } from '../constants.mjs';
4
+ import { resolveProjectRoot, resolveSourceRoot } from '../core/paths.mjs';
5
+ import { readLock } from '../core/lockfile.mjs';
6
+ import { parseCommonFlags } from '../cli/flags.mjs';
7
+ import { executeInstall } from './install.mjs';
8
+ import { VERSION } from '../version.mjs';
9
+ import { createLogger } from '../core/logger.mjs';
10
+
11
+ export async function runUpdate(argv, { log } = {}) {
12
+ const opts = parseCommonFlags(argv);
13
+ const logger = log ?? createLogger();
14
+ if (opts.help) {
15
+ logger.result(`Usage: qa update [--project <dir>] [--json] [--dry-run]
16
+
17
+ Update the installed pack from the current source tree (checkout or npm pack).
18
+ After registry publish, this will resolve the latest published version.
19
+ Always re-validates after updating.`);
20
+ return EXIT.OK;
21
+ }
22
+
23
+ const root = resolveProjectRoot(opts.project ?? process.cwd());
24
+ const sourceRoot = resolveSourceRoot();
25
+ const lock = readLock(root);
26
+ const installedVersion = lock?.pack?.version ?? null;
27
+
28
+ if (!opts.json) {
29
+ logger.step(`pack source: ${sourceRoot}`);
30
+ logger.step(`available version: ${VERSION}`);
31
+ if (installedVersion) logger.step(`installed version: ${installedVersion}`);
32
+ else logger.warn('no prior install found — will install fresh');
33
+ if (installedVersion === VERSION) {
34
+ logger.info('already on the source version — refreshing files for integrity');
35
+ }
36
+ logger.info(
37
+ 'Note: registry-based updates activate after the package is published to npm.',
38
+ );
39
+ }
40
+
41
+ const agentIds = opts.agents.length
42
+ ? opts.agents
43
+ : (lock?.agents ?? []).map((a) => a.id).filter(Boolean);
44
+
45
+ const result = await executeInstall({
46
+ projectRoot: root,
47
+ agentIds,
48
+ force: true,
49
+ dryRun: opts.dryRun,
50
+ json: opts.json,
51
+ log: logger,
52
+ });
53
+
54
+ if (!opts.json) {
55
+ logger.ok(`updated to ${VERSION}`);
56
+ }
57
+
58
+ logger.result({
59
+ ok: true,
60
+ dryRun: opts.dryRun,
61
+ previousVersion: installedVersion,
62
+ version: VERSION,
63
+ sourceRoot,
64
+ registryNote: 'npm registry updates available after publish',
65
+ install: result,
66
+ });
67
+
68
+ return EXIT.OK;
69
+ }
@@ -0,0 +1,62 @@
1
+ // `qa verify` — compare on-disk files to qa-lock.json hashes.
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { EXIT, LOCKFILE } from '../constants.mjs';
6
+ import { resolveProjectRoot } from '../core/paths.mjs';
7
+ import { readLock } from '../core/lockfile.mjs';
8
+ import { hashFile } from '../core/hash.mjs';
9
+ import { verifyError } from '../core/errors.mjs';
10
+ import { createLogger } from '../core/logger.mjs';
11
+ import { parseCommonFlags } from '../cli/flags.mjs';
12
+
13
+ export async function runVerify(argv, { log } = {}) {
14
+ const opts = parseCommonFlags(argv);
15
+ const logger = log ?? createLogger();
16
+ if (opts.help) {
17
+ logger.result(`Usage: qa verify [--project <dir>] [--json]
18
+
19
+ Verify installed files match qa-lock.json.
20
+ On failure: run qa repair`);
21
+ return EXIT.OK;
22
+ }
23
+
24
+ const root = resolveProjectRoot(opts.project ?? process.cwd());
25
+ const lock = readLock(root);
26
+ if (!lock) {
27
+ if (!opts.json) {
28
+ logger.error(`FAIL no ${LOCKFILE}`);
29
+ logger.info(' → run: qa install');
30
+ }
31
+ throw verifyError(`no ${LOCKFILE} in ${root}`, 'run: qa install');
32
+ }
33
+
34
+ const problems = [];
35
+ for (const entry of lock.files) {
36
+ const abs = path.join(root, entry.path);
37
+ if (!fs.existsSync(abs)) {
38
+ problems.push({ path: entry.path, reason: 'missing' });
39
+ continue;
40
+ }
41
+ const actual = hashFile(abs);
42
+ if (actual !== entry.sha256) {
43
+ problems.push({ path: entry.path, reason: 'hash mismatch' });
44
+ }
45
+ }
46
+
47
+ if (problems.length > 0) {
48
+ if (!opts.json) {
49
+ logger.error(`FAIL ${problems.length} drift(s) detected`);
50
+ for (const p of problems.slice(0, 20)) logger.info(` ${p.path}: ${p.reason}`);
51
+ if (problems.length > 20) logger.info(` … and ${problems.length - 20} more`);
52
+ logger.info(' → run: qa repair');
53
+ }
54
+ throw verifyError('installed files drifted from qa-lock.json', 'run: qa repair');
55
+ }
56
+
57
+ if (!opts.json) {
58
+ logger.ok(`PASS verified ${lock.files.length} file(s)`);
59
+ }
60
+ logger.result({ ok: true, files: lock.files.length, pack: lock.pack });
61
+ return EXIT.OK;
62
+ }
@@ -0,0 +1,34 @@
1
+ // Names and paths shared across the installer. Kept in one place so the CLI,
2
+ // commands, and tests agree on where things live.
3
+
4
+ export const PACK_NAME = 'qa-engineer';
5
+
6
+ // Files the installer writes into the target project root.
7
+ export const CONFIG_FILE = 'qa.config.json';
8
+ export const LOCKFILE = 'qa-lock.json';
9
+
10
+ // The pack's private working directory inside a target project.
11
+ export const QA_DIR = '.qa';
12
+ export const BACKUP_DIR = `${QA_DIR}/backups`;
13
+
14
+ // Marker line prepended to every generated invocation wrapper. Wrappers are
15
+ // disposable build artifacts; this line tells humans and `verify` that the file
16
+ // is pack-owned and regenerated, never hand-edited.
17
+ export const GENERATED_MARKER =
18
+ 'Generated by qa-engineer — do not edit. Regenerate with: qa install --force';
19
+
20
+ // Process exit codes. Stable so scripts and CI can branch on them.
21
+ export const EXIT = {
22
+ OK: 0,
23
+ FAILURE: 1, // an operation failed
24
+ USAGE: 2, // the command line was wrong
25
+ CONFLICT: 3, // refused to overwrite un-owned files without --force
26
+ VERIFY: 4, // installed files drifted from the lockfile
27
+ ENVIRONMENT: 5, // the environment cannot support the requested operation
28
+ };
29
+
30
+ // The two canonical skill discovery directories. Every target agent reads one
31
+ // of these; agent-specific directories are alternatives, not additions, so the
32
+ // planner installs each skill once per unique directory (no duplicates).
33
+ export const SHARED_SKILLS_DIR = '.agents/skills';
34
+ export const CLAUDE_SKILLS_DIR = '.claude/skills';
@@ -0,0 +1,124 @@
1
+ // Bundling deterministic tooling, integrated into installation. Skills that run
2
+ // a Python engine need that engine to travel with them (self-containment). The
3
+ // installer materializes it by COPYING the canonical code from shared/ into the
4
+ // installed skill's scripts/lib/ — a pure file copy, no code executed at install
5
+ // time, honoring the SECURITY.md guarantee. This mirrors what
6
+ // scripts/bundle_python.py does for repository development; users never run it.
7
+ //
8
+ // Deep verification (that the bundle *runs* under the user's interpreter)
9
+ // requires running Python and therefore lives in `qa doctor`, an explicit,
10
+ // user-invoked diagnostic — never in the install path.
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { spawnSync } from 'node:child_process';
15
+ import {
16
+ BUNDLE_MANIFEST,
17
+ BUNDLE_SOURCES,
18
+ BUNDLE_PACKAGE_DATA,
19
+ BUNDLE_MODULE_SOURCES,
20
+ BUNDLE_LAUNCHER,
21
+ BUNDLE_DEST,
22
+ } from './manifest.mjs';
23
+ import { listFilesRelative } from './paths.mjs';
24
+
25
+ /** Bundle file entries for one skill, relative to the skill directory. */
26
+ export function bundleFilesForSkill(sourceRoot, skill) {
27
+ const entry = BUNDLE_MANIFEST[skill];
28
+ if (!entry) return [];
29
+ const entries = [];
30
+
31
+ for (const pkg of entry.packages) {
32
+ const src = path.join(sourceRoot, BUNDLE_SOURCES[pkg]);
33
+ if (!fs.existsSync(src)) {
34
+ throw new Error(`bundled package source missing: ${BUNDLE_SOURCES[pkg]} (needed by ${skill})`);
35
+ }
36
+ for (const rel of listFilesRelative(src)) {
37
+ entries.push({
38
+ rel: `${BUNDLE_DEST}/${pkg}/${rel}`,
39
+ content: fs.readFileSync(path.join(src, rel)),
40
+ });
41
+ }
42
+ // Package data the code reads at runtime (internal schemas). Without these
43
+ // the diagnostics engine raises on every diagnosis.
44
+ for (const data of BUNDLE_PACKAGE_DATA[pkg] ?? []) {
45
+ const dataSrc = path.join(sourceRoot, data.from);
46
+ if (!fs.existsSync(dataSrc)) {
47
+ throw new Error(`bundled package data missing: ${data.from} (needed by ${skill})`);
48
+ }
49
+ for (const rel of listFilesRelative(dataSrc)) {
50
+ entries.push({
51
+ rel: `${BUNDLE_DEST}/${pkg}/${data.to}/${rel}`,
52
+ content: fs.readFileSync(path.join(dataSrc, rel)),
53
+ });
54
+ }
55
+ }
56
+ }
57
+
58
+ // The launcher: the documented entry point, beside lib/ rather than inside it.
59
+ const launcherSrc = path.join(sourceRoot, BUNDLE_LAUNCHER.from);
60
+ if (!fs.existsSync(launcherSrc)) {
61
+ throw new Error(`bundled launcher missing: ${BUNDLE_LAUNCHER.from} (needed by ${skill})`);
62
+ }
63
+ entries.push({ rel: BUNDLE_LAUNCHER.to, content: fs.readFileSync(launcherSrc) });
64
+
65
+ for (const mod of entry.modules) {
66
+ const src = path.join(sourceRoot, BUNDLE_MODULE_SOURCES[mod]);
67
+ if (!fs.existsSync(src)) {
68
+ throw new Error(`bundled module source missing: ${BUNDLE_MODULE_SOURCES[mod]} (needed by ${skill})`);
69
+ }
70
+ entries.push({
71
+ rel: `${BUNDLE_DEST}/${path.basename(src)}`,
72
+ content: fs.readFileSync(src),
73
+ });
74
+ }
75
+
76
+ return entries;
77
+ }
78
+
79
+ /** True if any skill carries bundled tooling (used to gate doctor's deep check). */
80
+ export function packHasBundles() {
81
+ return Object.keys(BUNDLE_MANIFEST).length > 0;
82
+ }
83
+
84
+ /** Locate a Python 3 interpreter, or null. Used only by doctor. */
85
+ export function findPython() {
86
+ for (const candidate of ['python3', 'python']) {
87
+ const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
88
+ if (probe.status === 0) {
89
+ const version = (probe.stdout || probe.stderr).trim();
90
+ return { bin: candidate, version };
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
96
+ // Runs inside the bundle with only the bundle on PYTHONPATH. Importing is not
97
+ // enough: a missing data file only surfaces when the engine actually runs, which
98
+ // is exactly the failure this check exists to catch.
99
+ const SMOKE = `
100
+ import json, sys
101
+ names = json.loads(sys.argv[1])
102
+ for name in names:
103
+ __import__(name)
104
+ if "qa_diagnostics" in names:
105
+ from qa_diagnostics import engine
106
+ diagnosis = engine.diagnose({
107
+ "tests": {"total": 1, "passed": 0, "failed": 1, "skipped": 0},
108
+ "executed": [{"title": "t", "status": "failed", "message": "no such element: #cart",
109
+ "file": "t.spec.ts", "retries": 0}],
110
+ })
111
+ assert diagnosis["entries"], "engine produced no diagnosis entries"
112
+ `;
113
+
114
+ /**
115
+ * Verify bundled tooling RUNS using ONLY its own directory on PYTHONPATH —
116
+ * proving self-containment under the user's interpreter. Doctor-only.
117
+ */
118
+ export function verifyImports({ pythonBin, libDir, packages }) {
119
+ const result = spawnSync(pythonBin, ['-c', SMOKE, JSON.stringify(packages)], {
120
+ env: { PYTHONPATH: libDir, PATH: process.env.PATH ?? '' },
121
+ encoding: 'utf8',
122
+ });
123
+ return { ok: result.status === 0, stderr: (result.stderr ?? '').trim() };
124
+ }
@@ -0,0 +1,73 @@
1
+ // Loading, validating, and defaulting qa.config.json. The file is optional;
2
+ // when absent, DEFAULTS apply. When present, it is validated against the
3
+ // published schema and merged over the defaults.
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { CONFIG_FILE } from '../constants.mjs';
9
+ import { QaError, usageError } from './errors.mjs';
10
+ import { validate } from './schema-validate.mjs';
11
+
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ const SCHEMA_PATH = path.join(here, '..', '..', 'schemas', 'qa.config.schema.json');
14
+
15
+ export const CONFIG_SCHEMA = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
16
+
17
+ // Every documented default in one place. The effective configuration is these
18
+ // values with any qa.config.json overrides applied on top.
19
+ export const DEFAULTS = Object.freeze({
20
+ version: 1,
21
+ frameworks: {},
22
+ browser: { default: 'chromium' },
23
+ output: { directory: 'qa-artifacts' },
24
+ logging: { level: 'info' },
25
+ telemetry: { enabled: false },
26
+ plugins: [],
27
+ agents: [],
28
+ });
29
+
30
+ function deepMerge(base, override) {
31
+ const result = Array.isArray(base) ? [...base] : { ...base };
32
+ for (const [key, value] of Object.entries(override ?? {})) {
33
+ if (value && typeof value === 'object' && !Array.isArray(value) && typeof result[key] === 'object') {
34
+ result[key] = deepMerge(result[key], value);
35
+ } else {
36
+ result[key] = value;
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+
42
+ /** The starter config written by `qa init`. Minimal on purpose — only version. */
43
+ export function starterConfig() {
44
+ return { version: 1 };
45
+ }
46
+
47
+ /** Validate a raw config object; throws a usage error listing every problem. */
48
+ export function validateConfig(raw, source = CONFIG_FILE) {
49
+ const errors = validate(raw, CONFIG_SCHEMA);
50
+ if (errors.length > 0) {
51
+ throw usageError(`${source} is invalid:\n - ${errors.join('\n - ')}`);
52
+ }
53
+ return raw;
54
+ }
55
+
56
+ /**
57
+ * Load the effective configuration for a project. Returns { config, source }
58
+ * where source is the file path when one was read, or null for defaults.
59
+ */
60
+ export function loadConfig(projectRoot) {
61
+ const file = path.join(projectRoot, CONFIG_FILE);
62
+ if (!fs.existsSync(file)) {
63
+ return { config: structuredClone(DEFAULTS), source: null };
64
+ }
65
+ let raw;
66
+ try {
67
+ raw = JSON.parse(fs.readFileSync(file, 'utf8'));
68
+ } catch (error) {
69
+ throw new QaError(`${CONFIG_FILE} is not valid JSON: ${error.message}`);
70
+ }
71
+ validateConfig(raw, file);
72
+ return { config: deepMerge(DEFAULTS, raw), source: file };
73
+ }
@@ -0,0 +1,29 @@
1
+ // Conflict detection. The installer must never silently overwrite a file it
2
+ // does not own. A conflict is an on-disk file that (a) we are about to write,
3
+ // (b) differs from what we would write, and (c) was not recorded as pack-owned
4
+ // by a previous install. Files owned by the prior lockfile are ours to update;
5
+ // byte-identical files are a no-op, not a conflict.
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { hashFile } from './hash.mjs';
10
+
11
+ /**
12
+ * @param {object} args
13
+ * @param {string} args.projectRoot
14
+ * @param {Array<{path:string, sha256:string}>} args.planned files we intend to write
15
+ * @param {object|null} args.priorLock the previous lockfile, if any
16
+ * @returns {Array<{path:string, reason:string}>} conflicts (empty = safe)
17
+ */
18
+ export function detectConflicts({ projectRoot, planned, priorLock }) {
19
+ const owned = new Set((priorLock?.files ?? []).map((f) => f.path));
20
+ const conflicts = [];
21
+ for (const entry of planned) {
22
+ const abs = path.join(projectRoot, entry.path);
23
+ if (!fs.existsSync(abs)) continue; // brand-new file, no conflict
24
+ if (owned.has(entry.path)) continue; // pack-owned, safe to update
25
+ if (hashFile(abs) === entry.sha256) continue; // identical content, no-op
26
+ conflicts.push({ path: entry.path, reason: 'exists and is not owned by a previous install' });
27
+ }
28
+ return conflicts;
29
+ }