@roopesh.yadava/qa-pack 1.5.0 → 1.6.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.
- package/README.md +22 -0
- package/claude/skills/SKILLS_CONTEXT.md +23 -4
- package/claude/skills/accessibility-testing/SKILL.md +16 -0
- package/claude/skills/automation/SKILL.md +18 -0
- package/claude/skills/bug-reporting/SKILL.md +18 -1
- package/claude/skills/impacted-tests/SKILL.md +16 -1
- package/claude/skills/k6-framework-scaffold/SKILL.md +14 -0
- package/claude/skills/manual-testing/SKILL.md +116 -24
- package/claude/skills/manual-testing/TEST_DESIGN_GUIDE.md +200 -0
- package/claude/skills/mobile-automation/BDD_TEMPLATES.md +221 -0
- package/claude/skills/mobile-automation/LOCATOR_PATTERNS.md +205 -0
- package/claude/skills/mobile-automation/MOBILE_MCP_REFERENCE.md +234 -0
- package/claude/skills/mobile-automation/SKILL.md +611 -0
- package/claude/skills/qa-agent/SKILL.md +15 -0
- package/claude/skills/qa-agent/toolkit/qa-toolkit.cjs +192 -11
- package/claude/skills/roam-testing/SKILL.md +19 -0
- package/claude/skills/test-charter/SKILL.md +14 -0
- package/claude/skills/ui-test-figma/SKILL.md +15 -0
- package/package.json +1 -1
- package/templates/.env.example +13 -0
|
@@ -17,11 +17,35 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
20
21
|
const path = require('path');
|
|
21
22
|
const crypto = require('crypto');
|
|
22
23
|
const { spawnSync } = require('child_process');
|
|
23
24
|
|
|
24
25
|
const ROOT = process.cwd();
|
|
26
|
+
|
|
27
|
+
// ── .env loading ─────────────────────────────────────────────────────────
|
|
28
|
+
// Every doc in the pack tells users to put QA_* config in `.env`, but this is a
|
|
29
|
+
// plain `node` script invoked by skills via Bash — nothing exports that file into
|
|
30
|
+
// the environment, and the pack ships no dotenv. Without this, every
|
|
31
|
+
// `process.env.QA_*` read below is permanently undefined and the features gated
|
|
32
|
+
// on them (report-run, token tracking) are silent no-ops no matter what the user
|
|
33
|
+
// configures. Real env vars always win, so an explicit `export` still overrides.
|
|
34
|
+
function loadDotEnv() {
|
|
35
|
+
let raw;
|
|
36
|
+
try { raw = fs.readFileSync(path.join(ROOT, '.env'), 'utf8'); } catch { return; }
|
|
37
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
38
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
39
|
+
if (!m) continue; // blank line, comment, or malformed — skip
|
|
40
|
+
let val = m[2].trim();
|
|
41
|
+
// strip one layer of matching quotes; leave inner content untouched
|
|
42
|
+
if (val.length >= 2 && ((val[0] === '"' && val.endsWith('"')) || (val[0] === "'" && val.endsWith("'")))) {
|
|
43
|
+
val = val.slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
if (process.env[m[1]] === undefined) process.env[m[1]] = val;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
loadDotEnv();
|
|
25
49
|
const PRODUCT_CONTEXT_ROOT = path.join(ROOT, '.claude', 'skills', 'qa-agent', 'product_context');
|
|
26
50
|
const OUTPUTS_DIR = path.join(ROOT, 'outputs');
|
|
27
51
|
const TRUST_THRESHOLD = 5;
|
|
@@ -365,11 +389,17 @@ function cmdRiskScore(args) {
|
|
|
365
389
|
|
|
366
390
|
// ── trust: trust ratchet on auto-approve ──────────────────────────────────
|
|
367
391
|
|
|
368
|
-
|
|
392
|
+
// `namespace` (optional) routes to trust.{namespace}.json instead of trust.json — full
|
|
393
|
+
// isolation for a second automation surface (e.g. mobile-automation) on the same product,
|
|
394
|
+
// so its gate streaks never enter the min() that decides an unrelated pipeline's eligibility.
|
|
395
|
+
// Omitted → identical behavior to before this flag existed (trust.json, no callers changed).
|
|
396
|
+
function trustFile(product, namespace) {
|
|
397
|
+
return path.join(productDir(product), namespace ? `trust.${namespace}.json` : 'trust.json');
|
|
398
|
+
}
|
|
369
399
|
|
|
370
400
|
function cmdTrustRecord(args) {
|
|
371
401
|
requireArgs(args, ['product', 'gate', 'result']);
|
|
372
|
-
const file = trustFile(args.product);
|
|
402
|
+
const file = trustFile(args.product, args.namespace);
|
|
373
403
|
const store = readJson(file, {});
|
|
374
404
|
if (!store[args.gate]) store[args.gate] = { clean: 0, edited: 0, streak: 0 };
|
|
375
405
|
if (args.result === 'clean') { store[args.gate].clean++; store[args.gate].streak++; }
|
|
@@ -380,7 +410,7 @@ function cmdTrustRecord(args) {
|
|
|
380
410
|
|
|
381
411
|
function cmdTrustStatus(args) {
|
|
382
412
|
requireArgs(args, ['product']);
|
|
383
|
-
const store = readJson(trustFile(args.product), {});
|
|
413
|
+
const store = readJson(trustFile(args.product, args.namespace), {});
|
|
384
414
|
const gates = Object.keys(store);
|
|
385
415
|
if (!gates.length) return console.log('NOT_ELIGIBLE — no gate history yet for this product.');
|
|
386
416
|
const minStreak = Math.min(...gates.map((g) => store[g].streak || 0));
|
|
@@ -500,9 +530,12 @@ function escapeHtml(s) {
|
|
|
500
530
|
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
501
531
|
}
|
|
502
532
|
|
|
503
|
-
function cmdDashboard() {
|
|
533
|
+
function cmdDashboard(args) {
|
|
504
534
|
const products = listProductFolders();
|
|
505
|
-
if (!products.length)
|
|
535
|
+
if (!products.length) {
|
|
536
|
+
if (args && args.json) return console.log(JSON.stringify({ products: 0, totalRuns: 0, totalOpenBugs: 0, repos: [] }));
|
|
537
|
+
return console.log('NO_PRODUCTS — no product has run yet.');
|
|
538
|
+
}
|
|
506
539
|
const stats = products.map(collectProductStats).sort((a, b) => b.runCount - a.runCount);
|
|
507
540
|
const totalRuns = stats.reduce((a, s) => a + s.runCount, 0);
|
|
508
541
|
const totalOpenBugs = stats.reduce((a, s) => a + s.openBugs, 0);
|
|
@@ -553,6 +586,21 @@ function cmdDashboard() {
|
|
|
553
586
|
|
|
554
587
|
ensureDir(OUTPUTS_DIR);
|
|
555
588
|
fs.writeFileSync(path.join(OUTPUTS_DIR, 'dashboard.html'), html);
|
|
589
|
+
|
|
590
|
+
if (args && args.json) {
|
|
591
|
+
console.log(JSON.stringify({
|
|
592
|
+
products: products.length,
|
|
593
|
+
totalRuns,
|
|
594
|
+
totalOpenBugs,
|
|
595
|
+
htmlPath: 'outputs/dashboard.html',
|
|
596
|
+
repos: stats.map((s) => ({
|
|
597
|
+
product: s.product, name: s.name, env: s.env, runCount: s.runCount,
|
|
598
|
+
avgReuse: s.avgReuse, flowCount: s.flowCount, openBugs: s.openBugs, bugCount: s.bugCount,
|
|
599
|
+
lastRunDate: s.lastRun ? s.lastRun['Date'] : null, lastRunCard: s.lastRun ? s.lastRun['Card'] : null,
|
|
600
|
+
})),
|
|
601
|
+
}));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
556
604
|
console.log(`Dashboard saved: outputs/dashboard.html (${products.length} products, ${totalRuns} runs, ${totalOpenBugs} open bugs)`);
|
|
557
605
|
}
|
|
558
606
|
|
|
@@ -569,11 +617,15 @@ function withinDays(dateStr, days) {
|
|
|
569
617
|
function cmdDigest(args) {
|
|
570
618
|
const days = parseInt(args.days, 10) || 7;
|
|
571
619
|
const products = listProductFolders();
|
|
572
|
-
if (!products.length)
|
|
620
|
+
if (!products.length) {
|
|
621
|
+
if (args && args.json) return console.log(JSON.stringify({ days, totalRuns: 0, totalBugs: 0, activeProducts: 0, totalProducts: 0, products: [] }));
|
|
622
|
+
return console.log('NO_PRODUCTS');
|
|
623
|
+
}
|
|
573
624
|
|
|
574
625
|
let totalRuns = 0;
|
|
575
626
|
let totalBugs = 0;
|
|
576
627
|
const sections = [];
|
|
628
|
+
const productsOut = [];
|
|
577
629
|
for (const product of products) {
|
|
578
630
|
const info = getProductInfo(product);
|
|
579
631
|
const runs = getTable(product, 'Runs Log').rows.filter((r) => withinDays(r['Date'], days));
|
|
@@ -582,10 +634,12 @@ function cmdDigest(args) {
|
|
|
582
634
|
const bugsFiled = runs.reduce((sum, r) => sum + bugsFiledCount(r), 0);
|
|
583
635
|
totalBugs += bugsFiled;
|
|
584
636
|
const avgReuse = avg(pctList(runs, 'Reuse %'));
|
|
637
|
+
const cards = runs.map((r) => r['Card']);
|
|
585
638
|
sections.push(
|
|
586
|
-
`### ${info.name || product}\n- Runs: ${runs.length} (${
|
|
639
|
+
`### ${info.name || product}\n- Runs: ${runs.length} (${cards.join(', ')})\n` +
|
|
587
640
|
`- Bugs filed: ${bugsFiled}\n- Avg reuse: ${avgReuse !== null ? avgReuse + '%' : 'n/a'}\n`
|
|
588
641
|
);
|
|
642
|
+
productsOut.push({ product, name: info.name || product, runCount: runs.length, cards, bugsFiled, avgReuse });
|
|
589
643
|
}
|
|
590
644
|
|
|
591
645
|
const today = todayStr();
|
|
@@ -598,6 +652,16 @@ function cmdDigest(args) {
|
|
|
598
652
|
ensureDir(OUTPUTS_DIR);
|
|
599
653
|
const file = path.join(OUTPUTS_DIR, `qa-weekly-digest-${today}.md`);
|
|
600
654
|
fs.writeFileSync(file, md);
|
|
655
|
+
|
|
656
|
+
if (args && args.json) {
|
|
657
|
+
console.log(JSON.stringify({
|
|
658
|
+
days, totalRuns, totalBugs,
|
|
659
|
+
activeProducts: sections.length, totalProducts: products.length,
|
|
660
|
+
mdPath: `outputs/qa-weekly-digest-${today}.md`,
|
|
661
|
+
products: productsOut,
|
|
662
|
+
}));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
601
665
|
console.log(
|
|
602
666
|
`Digest saved: outputs/qa-weekly-digest-${today}.md — ${totalRuns} runs, ${totalBugs} bugs across ` +
|
|
603
667
|
`${sections.length} active product(s) in the last ${days} days.`
|
|
@@ -606,9 +670,12 @@ function cmdDigest(args) {
|
|
|
606
670
|
|
|
607
671
|
// ── roi: token-spend / ROI view ────────────────────────────────────────────
|
|
608
672
|
|
|
609
|
-
function cmdRoi() {
|
|
673
|
+
function cmdRoi(args) {
|
|
610
674
|
const products = listProductFolders();
|
|
611
|
-
if (!products.length)
|
|
675
|
+
if (!products.length) {
|
|
676
|
+
if (args && args.json) return console.log(JSON.stringify({ totalRuns: 0, totalBugs: 0, hasTracking: false, rows: [] }));
|
|
677
|
+
return console.log('NO_PRODUCTS');
|
|
678
|
+
}
|
|
612
679
|
|
|
613
680
|
const tracking = tryReadTrackingLog();
|
|
614
681
|
const hasTracking = Array.isArray(tracking) && tracking.length > 0;
|
|
@@ -660,12 +727,125 @@ function cmdRoi() {
|
|
|
660
727
|
|
|
661
728
|
ensureDir(OUTPUTS_DIR);
|
|
662
729
|
fs.writeFileSync(path.join(OUTPUTS_DIR, 'roi-report.md'), md);
|
|
730
|
+
|
|
731
|
+
if (args && args.json) {
|
|
732
|
+
console.log(JSON.stringify({
|
|
733
|
+
totalRuns, totalBugs, hasTracking,
|
|
734
|
+
avgTokensPerRun: hasTracking && tokenRuns ? Math.round(totalTokens / tokenRuns) : null,
|
|
735
|
+
mdPath: 'outputs/roi-report.md',
|
|
736
|
+
rows: rowsOut,
|
|
737
|
+
}));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
663
740
|
console.log(
|
|
664
741
|
`ROI report saved: outputs/roi-report.md — ${totalRuns} runs, ${totalBugs} bugs` +
|
|
665
742
|
`${hasTracking ? ', token data included' : ' (token data unavailable — set QA_TRACKING_DIR to enable)'}.`
|
|
666
743
|
);
|
|
667
744
|
}
|
|
668
745
|
|
|
746
|
+
// ── report-run: optional team dashboard reporting (git-based, silent opt-in) ──
|
|
747
|
+
//
|
|
748
|
+
// Mirrors the QA_TRACKING_DIR convention (see SKILLS_CONTEXT.md "Token Tracking"):
|
|
749
|
+
// unset config = silent no-op, never mention it, never retry, never break the run.
|
|
750
|
+
// When configured, pushes one JSON file per run to a shared GitHub repo via the
|
|
751
|
+
// Contents API (a plain create — no local git commit/push in the calling repo, no
|
|
752
|
+
// read-modify-write race with other teammates' concurrent runs).
|
|
753
|
+
|
|
754
|
+
function gitConfigValue(key) {
|
|
755
|
+
const res = spawnSync('git', ['config', key], { cwd: ROOT, encoding: 'utf8' });
|
|
756
|
+
return res.status === 0 ? res.stdout.trim() : '';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function currentRepoName() {
|
|
760
|
+
const remote = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: ROOT, encoding: 'utf8' });
|
|
761
|
+
if (remote.status === 0 && remote.stdout.trim()) {
|
|
762
|
+
return remote.stdout.trim().replace(/\.git$/, '').split(/[/:]/).pop();
|
|
763
|
+
}
|
|
764
|
+
return path.basename(ROOT);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function cmdReportRun(args) {
|
|
768
|
+
const repo = process.env.QA_DASHBOARD_REPO;
|
|
769
|
+
const token = process.env.QA_DASHBOARD_TOKEN;
|
|
770
|
+
if (!repo || !token) return; // team dashboard not configured — silent no-op
|
|
771
|
+
|
|
772
|
+
// Only --skill and --outcome are universal. Half the skills in the pack have no
|
|
773
|
+
// Jira card at all (impacted-tests, k6-framework-scaffold, test-charter, and
|
|
774
|
+
// roam-testing when run without one) and some have no product context, so
|
|
775
|
+
// requiring either would make "log every skill" impossible to satisfy.
|
|
776
|
+
requireArgs(args, ['skill', 'outcome']);
|
|
777
|
+
|
|
778
|
+
// No freeform --notes flag: only short, structurally-constrained values (product/card/
|
|
779
|
+
// phase/outcome/bug IDs) are safe to inline into the caller's shell command — see the
|
|
780
|
+
// "Never inline arbitrary text" rule in SKILLS_CONTEXT.md.
|
|
781
|
+
const payload = {
|
|
782
|
+
product: args.product || currentRepoName(),
|
|
783
|
+
card: args.card || null,
|
|
784
|
+
// One record per skill execution. `skill` is what ran (manual-testing,
|
|
785
|
+
// automation, ...); `phase` stays for backward compat with records already
|
|
786
|
+
// in the dashboard repo, defaulting to the skill name when a caller omits it.
|
|
787
|
+
skill: args.skill,
|
|
788
|
+
phase: args.phase || args.skill,
|
|
789
|
+
outcome: args.outcome,
|
|
790
|
+
bugsFiled: String(args.bugs || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
791
|
+
reusePct: args.reuse !== undefined ? parseFloat(args.reuse) : null,
|
|
792
|
+
date: args.date || todayStr(),
|
|
793
|
+
// Display name only — never user.email. The dashboard repo is public, so an
|
|
794
|
+
// email here is published permanently in git history; the "runs by teammate"
|
|
795
|
+
// panel only ever needs a label. QA_DASHBOARD_AUTHOR overrides.
|
|
796
|
+
author: process.env.QA_DASHBOARD_AUTHOR || gitConfigValue('user.name') || os.userInfo().username,
|
|
797
|
+
repo: currentRepoName(),
|
|
798
|
+
reportedAt: new Date().toISOString(),
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// These land in a URL path, so restrict them to a safe charset rather than
|
|
802
|
+
// trusting the caller — a stray `/` or `..` in a product name would otherwise
|
|
803
|
+
// write outside runs/ in the dashboard repo.
|
|
804
|
+
const seg = (v, fallback) => String(v || fallback).replace(/[^A-Za-z0-9._-]/g, '-').replace(/^[.-]+/, '') || fallback;
|
|
805
|
+
const filePath = `runs/${seg(payload.product, 'unknown')}/${payload.date}-${seg(payload.card, 'no-card')}-${seg(payload.skill, 'skill')}-${crypto.randomBytes(4).toString('hex')}.json`;
|
|
806
|
+
const content = Buffer.from(JSON.stringify(payload, null, 2)).toString('base64');
|
|
807
|
+
|
|
808
|
+
const controller = new AbortController();
|
|
809
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
810
|
+
try {
|
|
811
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/contents/${filePath}`, {
|
|
812
|
+
method: 'PUT',
|
|
813
|
+
signal: controller.signal,
|
|
814
|
+
headers: {
|
|
815
|
+
Authorization: `Bearer ${token}`,
|
|
816
|
+
Accept: 'application/vnd.github+json',
|
|
817
|
+
'Content-Type': 'application/json',
|
|
818
|
+
'User-Agent': 'qa-pack-toolkit',
|
|
819
|
+
},
|
|
820
|
+
body: JSON.stringify({ message: `run: ${payload.product} ${payload.card}`, content }),
|
|
821
|
+
});
|
|
822
|
+
// A 401 (dead/expired PAT — fine-grained tokens cap at 1 year), a 404 (wrong
|
|
823
|
+
// QA_DASHBOARD_REPO, or a token without Contents:write) and a success are all
|
|
824
|
+
// indistinguishable if the status is never read. Stay silent on stdout so a
|
|
825
|
+
// broken dashboard can't derail a QA run, but always leave a trace on disk.
|
|
826
|
+
if (!res.ok) reportRunFailure(args, `HTTP ${res.status} ${res.statusText} — ${(await res.text().catch(() => '')).slice(0, 200)}`);
|
|
827
|
+
else if (args.verbose) console.log(`report-run OK → ${repo}/${filePath}`);
|
|
828
|
+
} catch (err) {
|
|
829
|
+
reportRunFailure(args, err && err.name === 'AbortError' ? 'timed out after 5s' : String(err && err.message || err));
|
|
830
|
+
} finally {
|
|
831
|
+
clearTimeout(timeout);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Failure trail for report-run. stdout stays clean (the calling skill must never
|
|
836
|
+
// print or retry), so the evidence goes to outputs/ — already gitignored — and to
|
|
837
|
+
// stdout only under --verbose, which is how you verify a fresh PAT once.
|
|
838
|
+
function reportRunFailure(args, detail) {
|
|
839
|
+
const line = `${new Date().toISOString()} report-run failed: ${detail}\n`;
|
|
840
|
+
if (args.verbose) console.log(line.trim());
|
|
841
|
+
try {
|
|
842
|
+
ensureDir(OUTPUTS_DIR);
|
|
843
|
+
fs.appendFileSync(path.join(OUTPUTS_DIR, 'report-run-errors.log'), line);
|
|
844
|
+
} catch {
|
|
845
|
+
// last resort: swallow — a dashboard problem must never fail a QA run
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
669
849
|
// ── dispatch ────────────────────────────────────────────────────────────
|
|
670
850
|
|
|
671
851
|
function main() {
|
|
@@ -688,6 +868,7 @@ function main() {
|
|
|
688
868
|
dashboard: cmdDashboard,
|
|
689
869
|
digest: cmdDigest,
|
|
690
870
|
roi: cmdRoi,
|
|
871
|
+
'report-run': cmdReportRun,
|
|
691
872
|
};
|
|
692
873
|
const handler = handlers[command];
|
|
693
874
|
if (!handler) {
|
|
@@ -695,11 +876,11 @@ function main() {
|
|
|
695
876
|
'Usage: qa-toolkit.cjs <command> [--flags]\n' +
|
|
696
877
|
'Commands: list-products, get-bugs, get-runs, get-selectors, fingerprint, pii-scan, dup-bug,\n' +
|
|
697
878
|
' cost-estimate, risk-score, trust-record, trust-status,\n' +
|
|
698
|
-
' locator-record, locator-query, dashboard, digest, roi'
|
|
879
|
+
' locator-record, locator-query, dashboard, digest, roi, report-run'
|
|
699
880
|
);
|
|
700
881
|
process.exit(command ? 1 : 0);
|
|
701
882
|
}
|
|
702
|
-
handler(args);
|
|
883
|
+
Promise.resolve(handler(args)).catch(() => {});
|
|
703
884
|
}
|
|
704
885
|
|
|
705
886
|
main();
|
|
@@ -185,3 +185,22 @@ If yes: `Skill: test-charter`, pre-filled with the roam report path just saved.
|
|
|
185
185
|
| A page 500s on load | Record as 🔴, do not retry, continue the loop |
|
|
186
186
|
| Page cap hit with queue still non-empty | Stop cleanly, report `{visited} of {discovered}` coverage — never silently keep going |
|
|
187
187
|
| `browser_network_requests` unavailable | Skip failed-request detection for that page, note it once in the report, continue |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Final Step — Report to team dashboard (optional, silent)
|
|
192
|
+
|
|
193
|
+
Always the last action of the run, after everything else has printed. This is a no-op
|
|
194
|
+
unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
|
|
195
|
+
about it, never retry, never treat a non-zero exit as a run failure.
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
|
|
199
|
+
--skill roam-testing --product {PRODUCT_FOLDER} --card {CARD_ID, omit if none} \
|
|
200
|
+
--outcome {Pass|Fail|Partial} --bugs "{comma-separated bug IDs, or omit if none}"
|
|
201
|
+
```
|
|
202
|
+
A card is optional in this skill (Step 0 allows `skip`) — omit `--card` entirely when
|
|
203
|
+
there isn't one rather than passing a placeholder. `report-run` takes only short, structurally-constrained values (product folder, card ID,
|
|
204
|
+
skill name, outcome, bug IDs). It has no `--notes` flag — never inline freeform text such as
|
|
205
|
+
a finding, a page title, or a bug description (see "Never inline arbitrary text" in
|
|
206
|
+
`SKILLS_CONTEXT.md`).
|
|
@@ -298,3 +298,17 @@ TEST CHARTER PUBLISHED
|
|
|
298
298
|
Published: [published_url or "URL not returned by API"]
|
|
299
299
|
✅ Uploaded to decision record
|
|
300
300
|
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Final Step — Report to team dashboard (optional, silent)
|
|
305
|
+
|
|
306
|
+
Always the last action of the run, after everything else has printed. This is a no-op
|
|
307
|
+
unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
|
|
308
|
+
about it, never retry, never treat a non-zero exit as a run failure.
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
|
|
312
|
+
--skill test-charter --outcome {Pass|Fail}
|
|
313
|
+
```
|
|
314
|
+
No product folder and no card — omit both flags.
|
|
@@ -236,3 +236,18 @@ JIRA UPDATED
|
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
Run `end + report + session` token close-out.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Final Step — Report to team dashboard (optional, silent)
|
|
243
|
+
|
|
244
|
+
Always the last action of the run, after everything else has printed. This is a no-op
|
|
245
|
+
unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
|
|
246
|
+
about it, never retry, never treat a non-zero exit as a run failure.
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
|
|
250
|
+
--skill ui-test-figma --card {CARD_ID, omit if none} \
|
|
251
|
+
--outcome {Pass|Fail|Partial} --bugs "{comma-separated bug IDs, or omit if none}"
|
|
252
|
+
```
|
|
253
|
+
Omit `--product` (no product folder in this skill) and omit `--card` when no card was given.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roopesh.yadava/qa-pack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "AI-powered QA agent skills for Claude Code — manual testing, BDD automation, accessibility, UI/Figma diff, bug reporting",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"postinstall": "node bin/postinstall.js"
|
package/templates/.env.example
CHANGED
|
@@ -24,3 +24,16 @@ JIRA_API_TOKEN=
|
|
|
24
24
|
# ── Optional: model-tier recommendation for qa-agent (see SKILLS_CONTEXT.md) ──
|
|
25
25
|
# recommend (default) | off | subagent (advanced — see "Model Routing" in SKILLS_CONTEXT.md)
|
|
26
26
|
# QA_MODEL_ROUTING=recommend
|
|
27
|
+
|
|
28
|
+
# ── Optional: team QA dashboard (shared, git-based, no server to host) ────────
|
|
29
|
+
# Point at the shared dashboard-data repo ("owner/repo") + a fine-grained PAT
|
|
30
|
+
# scoped to only that repo with Contents: Read and write access.
|
|
31
|
+
# When either is unset, runs are skipped silently — nothing is sent anywhere.
|
|
32
|
+
# Once set, EVERY skill reports its own execution as its last step.
|
|
33
|
+
# Note: if the dashboard repo is public, everything below is published permanently
|
|
34
|
+
# in its git history — product folder names, Jira card IDs, bug IDs and the author
|
|
35
|
+
# label. The author label is git config user.name (never your email); override it
|
|
36
|
+
# here if you'd rather publish something else, e.g. a first name or initials.
|
|
37
|
+
# QA_DASHBOARD_REPO=your-org/qa-dashboard-data
|
|
38
|
+
# QA_DASHBOARD_TOKEN=
|
|
39
|
+
# QA_DASHBOARD_AUTHOR=
|