@roopesh.yadava/qa-pack 1.5.1 → 1.6.1

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 CHANGED
@@ -92,6 +92,8 @@ and `/write-acceptance-criteria`, which are also available as explicit slash com
92
92
  | `/write-acceptance-criteria PROJ-123` | write-acceptance-criteria | Generates AC, appends to the Jira card description |
93
93
  | `/impacted-tests` / `which tests are impacted by this pull` | impacted-tests | After pulling dev changes into a test branch, reports which Cucumber feature files are at risk — report-only, no card needed |
94
94
  | `set up k6` / `scaffold performance tests` | k6-framework-scaffold | Scaffolds a `k6-performance-tests/` framework (Grafana Cloud, protocol + optional browser layers) with commented templates to fill in — no card needed |
95
+ | `set up mobile testing` / `set up mobilewright` / `scaffold mobile BDD tests` | mobilewright-setup | Scaffolds Cucumber.js + Mobilewright + a living-documentation HTML reporter into a mobile app repo (Expo, bare React Native, or native iOS/Android), plus one real smoke feature/step file — no card needed. Run once, before `mobile-automation` has anything to plug into |
96
+ | `automate mobile PROJ-123` / `write mobile automation for this card` | mobile-automation | Reuse-first Gherkin → step defs → Screen POM → real device run + self-heal for native iOS/Android apps, via Mobile MCP + Mobilewright — the mobile counterpart to `automation` (web) |
95
97
  | `roam mode` / `explore the app` | roam-testing | Card-free exploratory testing — capped breadth-first crawl, report + optional bug filing + charter |
96
98
  | `qa dashboard` / `weekly digest` / `token roi` | qa-insights | Cross-product health dashboard, weekly digest, or token-spend/ROI view — 100% script-generated, no card needed |
97
99
 
@@ -107,6 +109,28 @@ token beyond the one line of output. This is also why `qa-insights` (dashboard/d
107
109
  across every product you've ever tested) costs about the same whether you have 2 products or
108
110
  200 — the script does the aggregation, not the model.
109
111
 
112
+ ### Team dashboard (optional, off by default)
113
+
114
+ `qa-insights`' dashboard is local and per-repo — it only sees runs on your own machine.
115
+ To see the whole team's runs across every product, set `QA_DASHBOARD_REPO` and
116
+ `QA_DASHBOARD_TOKEN` in `.env` (see `.env.example`). Once set, **every skill** reports its
117
+ own execution as its last step — one small JSON file pushed to that shared GitHub repo via
118
+ the GitHub Contents API. No local `git commit`/`git push`, so it never touches this repo's
119
+ `.git` or shows up in `git status`. Leave the variables unset and nothing changes; the
120
+ call is a silent no-op, same as the optional `QA_TRACKING_DIR` token tracking.
121
+
122
+ One record is one *skill execution*, not one card: a single card taken through `qa-agent` →
123
+ `manual-testing` → `bug-reporting` reports three. The dashboard counts both ("skill runs"
124
+ and "cards covered") so neither number is misleading.
125
+
126
+ What each record contains: product, card (when the skill has one), skill name, outcome, bug
127
+ IDs, reuse %, date, calling repo name, and an author label taken from `git config user.name`
128
+ — never `user.email`, since the dashboard repo is typically public and its history is
129
+ permanent. Set `QA_DASHBOARD_AUTHOR` in `.env` to override the label. A push that fails
130
+ (expired PAT, wrong repo, no network) stays silent and is appended to
131
+ `outputs/report-run-errors.log`; add `--verbose` to a manual `report-run` call to see the
132
+ result directly, which is the way to verify a freshly issued token.
133
+
110
134
  ## What postinstall does
111
135
 
112
136
  | File | Behaviour |
@@ -1,185 +1,11 @@
1
- You are a Jira bug reporting assistant. Follow these steps precisely and in order.
2
-
3
- ---
4
-
5
- ## Step 1 — Select a project
6
-
7
- Use the Atlassian MCP tool (`getVisibleJiraProjects`) to fetch all visible projects immediately — do not wait for user input.
8
-
9
- Display the list and ask:
10
- "Which board are you working on? You can reply with the number or the board key:
11
- 1. [KEY] — [Project Name]
12
- 2. [KEY] — [Project Name]
13
- ..."
14
-
15
- Wait for the user to select a project by number or board key. Save the selected project key for the rest of the flow.
16
-
17
- ---
18
-
19
- ## Step 1A/1B — Ask card type
20
-
21
- Ask the user:
22
- "Is this an **Exploratory Bug** (create a new card) or an **Existing Card** (add bugs to an existing one)?"
23
-
24
- Wait for the user's choice before proceeding.
25
-
26
- - If the user chooses **Exploratory Bug** → go to Step 1A
27
- - If the user chooses **Existing Card** → go to Step 1B
28
-
29
- ---
30
-
31
- ## Step 1A — Create a new Exploratory Bug card
32
-
33
- Ask the user: "Please enter a name for the Exploratory Bug card."
34
-
35
- Wait for the user's input.
36
-
37
- Use the Atlassian MCP tool (`createJiraIssue`) to create a new Jira issue in the selected project with:
38
- - **Issue Type:** Bug
39
- - **Summary:** [Card name entered by the user]
40
- - **Labels:** Exploratory
41
-
42
- After creating the issue, use `getTransitionsForJiraIssue` to fetch available transitions, then use `transitionJiraIssue` to move the card to **Backlog** status.
43
-
44
- Once created and moved to Backlog, display:
45
-
46
- > **Exploratory Bug card created:** [CARD-NUMBER] — [Card Name]
47
- > [Link to card]
48
-
49
- Save this card number as the active card for the rest of the flow. Then proceed to Step 2.
50
-
51
- ---
52
-
53
- ## Step 1B — Use an existing card
54
-
55
- Ask the user: "Please enter the card number (e.g. 123 or [KEY]-123) to attach this bug report to."
56
-
57
- Wait for the user's input.
58
-
59
- If the user enters only a number (e.g. `2446`), construct the full card number using the selected project key (e.g. QE-2446).
60
-
61
- Use the Atlassian MCP tool (`getJiraIssue`) to fetch the card details. Display:
62
-
63
- > **Card found:** [CARD-NUMBER] — [Card Summary/Title]
64
-
65
- If the fetched card's issue type is **Bug**, ask:
66
- "Do you want to add the bugs to the **Description** or as a **Comment**?"
67
-
68
- Wait for the user's choice and save it as the **save mode** (Description or Comment) for use in Step 6.
69
-
70
- Save this card number as the active card for the rest of the flow. Then proceed to Step 2.
71
-
72
- ---
73
-
74
- ## Step 2 — Collect bug details
75
-
76
- Ask the user: "Please describe the bug."
77
-
78
- Wait for the user's input. Accept whatever they provide — a sentence, a paragraph, or structured text.
79
-
80
- From the input, infer and derive all necessary fields:
81
- - **Bug Title** — extract or summarise from what was given
82
- - **Expected Outcome** — infer from context if not explicitly stated
83
- - **Actual Outcome** — extract the described problem
84
- - **Steps to Reproduce** — extract or derive from the description
85
-
86
- Do NOT ask the user for any missing fields. Use what was given and proceed.
87
-
88
1
  ---
89
-
90
- ## Step 3Display the formatted bug report
91
-
92
- Show the bug report in this exact format before posting:
93
-
94
- ### Bug No. [n]: [Bug Title]
95
-
96
- ### Expected Outcome:
97
- [Expected Outcome]
98
-
99
- ### Actual Outcome:
100
- [Actual Outcome]
101
-
102
- ### Steps to Reproduce:
103
- [Steps]
104
-
105
- After showing the report, ask:
106
- "Is this the only bug, or do you have another bug to add?"
107
-
108
- - If the user says **yes (another bug)** — go back to Step 2 and collect the next bug. Append it to the report using the same format with an incremented Bug No. Repeat until the user says no more bugs.
109
- - If the user says **no more bugs** — proceed to Step 4.
110
-
2
+ name: bug-report
3
+ description: File a bug to Jira new Exploratory Bug card or attached to an existing card.
4
+ argument-hint: "[bug description | Jira card ID]"
5
+ user-invocable: true
111
6
  ---
112
7
 
113
- ## Step 4 — Ask who to mention
114
-
115
- Before posting, ask:
116
- "Who should I notify about this bug? Please enter the person's name, or type **none** to skip."
117
-
118
- Wait for the user's input.
119
-
120
- - If the user types **none** (or says no one / skip) → set mention as empty, skip Step 5, and proceed directly to Step 6. Do NOT add any mention or "Please check this" line in the report.
121
- - Otherwise → proceed to Step 5.
122
-
123
- ---
124
-
125
- ## Step 5 — Search for the person in Jira
126
-
127
- Use the Atlassian MCP tool (`lookupJiraAccountId`) to search for the name the user entered.
128
-
129
- If multiple results are found, list them and ask:
130
- "Is this the right person? [Name — Account ID]"
131
-
132
- If only one result is found, display that person's name and ask:
133
- "Found: [Full Name]. Is this the right person? (yes / no)"
134
-
135
- Wait for confirmation before proceeding.
136
-
137
- ---
138
-
139
- ## Step 6 — Save the bug report to Jira
140
-
141
- ### If the card is an Exploratory Bug (created in Step 1A):
142
-
143
- Use the Atlassian MCP tool (`editJiraIssue`) to update the **Description** field of the card with the full bug report AND the mention at the end.
144
-
145
- The description body must follow this structure:
146
-
147
- ```
148
- h3. Bug No. [n]: [Bug Title]
149
-
150
- h3. Expected Outcome:
151
- [Expected Outcome]
152
-
153
- h3. Actual Outcome:
154
- [Actual Outcome]
155
-
156
- h3. Steps to Reproduce:
157
- [Steps]
158
-
159
- (repeat the above block for each additional bug if more than one was collected)
160
-
161
- (only include the line below if a person was confirmed in Step 5 — omit it entirely if the user chose none)
162
- @[confirmed person's display name], Please check.
163
- ```
164
-
165
- Do NOT post a comment. Write everything into the Description field of the card.
166
-
167
- ---
168
-
169
- ### If the card is an Existing Card (from Step 1B):
170
-
171
- - If the user chose **Description** → use `editJiraIssue` to update the Description field with the full bug report and mention. Do NOT post a comment.
172
- - If the user chose **Comment** → use `addCommentToJiraIssue` to post one single comment with the full bug report and mention. Do NOT post a second separate comment.
173
-
174
- In both cases the content structure is the same as above.
175
-
176
- ---
177
-
178
- ## Step 7 — Confirm completion
179
-
180
- Show a summary:
181
- - Jira card: [Card number with link]
182
- - Bug report posted: ✓
183
- - Notified: [Confirmed person's name]
8
+ Invoke the `bug-reporting` skill.
184
9
 
185
- Done.
10
+ If the user provided an argument (e.g. a bug description or a Jira card ID), pass it
11
+ directly into the skill as the initial intent so it does not re-ask for input already given.
@@ -14,8 +14,9 @@ user-invocable: true
14
14
  ## Core Workflow
15
15
 
16
16
  0. **Detect Jira card ID** — If the argument matches the pattern `[A-Z]+-[0-9]+` (e.g. `FF-420`, `AA-12`),
17
- fetch the issue using the `getJiraIssue` tool with cloudId `853e134e-6574-491a-a075-1b06f7d4b478`.
18
- Use the issue summary and description as the feature input for the next steps.
17
+ fetch the issue using the `getJiraIssue` tool. Use the issue summary and description as the
18
+ feature input for the next steps. Store the card's own URL (returned by the fetch) for the
19
+ confirmation step later — never hardcode a Jira domain.
19
20
  If the issue has no description, use the summary only and note this in the Assumptions section.
20
21
  If the argument is not a Jira card ID, treat it as a raw feature description and skip to step 1.
21
22
 
@@ -33,8 +34,8 @@ user-invocable: true
33
34
  - Fetch the current description of the issue using `getJiraIssue`
34
35
  - Preserve the full existing description exactly as-is (do NOT remove or overwrite any content, including COS or any other existing sections)
35
36
  - Append the generated acceptance criteria at the very bottom of the existing description, separated by a divider line (`---`)
36
- - Update the issue description using `editJiraIssue` with cloudId `853e134e-6574-491a-a075-1b06f7d4b478`
37
- - After updating, confirm to the user with the Jira issue URL: `https://7edge.atlassian.net/browse/<CARD-ID>`
37
+ - Update the issue description using `editJiraIssue`
38
+ - After updating, confirm to the user with the card's own URL (from the fetch in step 0) — never a hardcoded domain
38
39
 
39
40
  ---
40
41
 
@@ -125,43 +126,3 @@ If the feature description is vague:
125
126
  ---
126
127
 
127
128
  *Assumed: web-based product with a registered user model and a transactional email provider already in place. Adjust if this is a mobile or API-only context.*
128
-
129
-
130
-
131
- <!-- Created by Roopesh Yadava
132
-
133
- Generate detailed documentation for the feature: {{feature_description}}
134
-
135
- Follow these steps:
136
-
137
- 1. **Identify scenarios** — Analyze the feature description and derive all possible user flows, including standard usage and edge cases.
138
-
139
- 2. **Understand expected behavior** — Determine:
140
-
141
- * What the user is trying to achieve
142
- * System responses for valid and invalid inputs
143
- * Possible failure points or UX gaps
144
-
145
- 3. **Generate output** with the following structure:
146
-
147
- ## Acceptance Criteria
148
-
149
- (Leave this section empty — do not list or rewrite anything here.)
150
-
151
- ## Happy Paths
152
-
153
- * List all happy paths using numbered bullets.
154
- * Each step should describe one clear user action or system result.
155
- * Keep steps concise, realistic, and sequential.
156
-
157
- ## Unhappy Paths
158
-
159
- * List all unhappy paths using numbered bullets.
160
- * Include:
161
-
162
- * User errors
163
- * Validation failures
164
- * System issues
165
- * Edge cases
166
- * Missing or incorrect feedback
167
- * Be specific and practical in each scenario. -->
@@ -96,6 +96,7 @@ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs <command> [--flags]
96
96
  | `trust-record` / `trust-status` --product P [--namespace N] | automation, mobile-automation (`--namespace mobile` — routes to a separate `trust.mobile.json` so its gate streaks never enter qa-agent's web-only eligibility computation), qa-agent (status) | Trust ratchet on Gate 1/Gate 2 approvals. `--namespace` is optional and additive — omitted, behavior is unchanged (`trust.json`); `trust-status` takes `min(streak)` across every gate in whichever file it reads, so gates from two automation surfaces must never share one file |
97
97
  | `locator-record` / `locator-query` --product P --page U | automation, mobile-automation (page keys prefixed `mobile:` to avoid colliding with web page URLs) | Self-improving locator memory across self-heal fixes |
98
98
  | `dashboard` / `digest [--days N]` / `roi` | qa-insights | Cross-product reports — 100% script-generated, zero synthesis. Add `--json` to any of the three for a structured payload (same data, machine-readable) instead of the one-line summary — for anything scripting against this (e.g. an external dashboard app) rather than chatting with it. |
99
+ | `report-run` --skill S --outcome Y [--product P] [--card C] [--bugs "a,b"] [--reuse N] [--phase X] [--verbose] | **every skill, as its own last step** — `qa-agent`, `automation`, `manual-testing`, `mobile-automation`, `accessibility-testing`, `ui-test-figma`, `roam-testing`, `bug-reporting`, `impacted-tests`, `test-charter`, `k6-framework-scaffold` | Optional team dashboard: pushes one JSON file per **skill execution** to a shared GitHub repo (Contents API — no local git commit/push). Only `--skill`/`--outcome` are required: `--product` defaults to the calling repo's name and `--card` is omitted entirely by the skills that have no Jira card. Silent no-op unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env`, same convention as `QA_TRACKING_DIR`. A failed push never prints or fails the run — it is appended to `outputs/report-run-errors.log`; `--verbose` prints the result, which is how you verify a new PAT once. No `--notes` flag — only short, structurally-constrained values are safe to inline. |
99
100
 
100
101
  This file is versioned logic (always overwritten on `npm update`, like every other skill
101
102
  file) — never store product data inside it. Its outputs live under
@@ -103,6 +104,11 @@ file) — never store product data inside it. Its outputs live under
103
104
  `locator-learnings.md` — all new, all gitignored the same way `context.md` already is) or
104
105
  `outputs/` (`dashboard.html`, `qa-weekly-digest-*.md`, `roi-report.md`).
105
106
 
107
+ The toolkit reads `.env` from the repo root itself (real environment variables win), so
108
+ every `QA_*` value documented for `.env` reaches it without the calling skill exporting
109
+ anything. Before this, `QA_DASHBOARD_*` and `QA_TRACKING_DIR` were read straight off
110
+ `process.env` and were therefore permanently unset in normal skill use.
111
+
106
112
  **Never inline arbitrary text into a toolkit shell call.** Bug descriptions, locator
107
113
  strings, and anything else that isn't a short agent-controlled token (a card ID, a product
108
114
  folder name, a URL) can contain quotes, `` ` ``, `$(...)`, or `|` — inlined into a bash
@@ -155,6 +161,13 @@ skill (same status as `roam-testing`/`k6-framework-scaffold`), not wired into qa
155
161
  Phase 2 dispatch, which remains web/Playwright-only via `automation`. Trigger it by name
156
162
  ("automate mobile PROJ-123", "mobile test PROJ-123") rather than through the qa-agent phase menu.
157
163
 
164
+ `mobilewright-setup` sits outside this diagram too — a one-time scaffolder (same role as
165
+ `k6-framework-scaffold`, but for a mobile app's BDD test stack instead of a perf-test stack),
166
+ not a Jira-card-driven skill and not wired into qa-agent. It's what a repo needs *before*
167
+ `mobile-automation` has anything to plug into — run it once on a repo with no existing
168
+ Cucumber/Mobilewright setup, then use `mobile-automation` for ongoing card-driven test
169
+ authoring against what it scaffolded.
170
+
158
171
  ## Skills — One-Line Summary
159
172
 
160
173
  | Skill | Input | Output | MCP Needed |
@@ -162,6 +175,7 @@ Phase 2 dispatch, which remains web/Playwright-only via `automation`. Trigger it
162
175
  | `qa-agent` | Jira card ID or menu choice | Dispatches to correct skill | Atlassian |
163
176
  | `automation` | Jira card ID | Reuse audit + Gherkin + Step Defs + POM + real run (reuse % reported) | Atlassian, Playwright |
164
177
  | `mobile-automation` | Jira card ID | Reuse audit + mobile Gherkin + Step Defs + Screen POM + real device run (reuse % reported) | Atlassian, Mobile MCP, MobileWright/mobilecli |
178
+ | `mobilewright-setup` | Nothing (a repo to scaffold into) — no card needed | Cucumber.js + Mobilewright + living-documentation HTML reporter installed and wired into `package.json`, plus one real smoke feature/step file | None — pure file scaffolding + `npx mobilewright doctor`/local test run |
165
179
  | `manual-testing` | Jira card ID + app URL | Execution report + bugs + charter | Atlassian, Playwright |
166
180
  | `ui-test-figma` | Figma URL + app URL | UI mismatch report, Jira comment | Playwright (CLI+MCP), Figma (optional) |
167
181
  | `accessibility-testing` | Full page URL + Jira card (optional) | WCAG 2.1 A/AA report + Jira bugs | Playwright (CLI+MCP), Atlassian |
@@ -294,6 +308,8 @@ First run will always be 0% (cold cache). Second and subsequent runs should cach
294
308
  | Token analytics | `~/.claude/token_analytics.png` |
295
309
  | Knowledge graph | `graphify-out/graph.html` (open in browser) |
296
310
  | Product QA context | `.claude/skills/qa-agent/product_context/[PRODUCT]/context.md` |
311
+ | Team QA dashboard data (shared, cross-repo, not in this repo) | `runs/[PRODUCT]/[date]-[card]-[skill]-[id].json` inside the repo named by `QA_DASHBOARD_REPO` — see `report-run` |
312
+ | Team dashboard push failures (local, gitignored) | `outputs/report-run-errors.log` — the only place a failed `report-run` is visible |
297
313
  | DOM fingerprint cache | `.claude/skills/qa-agent/product_context/[PRODUCT]/dom-fingerprints.json` |
298
314
  | Trust ratchet state | `.claude/skills/qa-agent/product_context/[PRODUCT]/trust.json` |
299
315
  | Locator learnings | `.claude/skills/qa-agent/product_context/[PRODUCT]/locator-learnings.md` |
@@ -315,3 +315,19 @@ Accessibility Testing Complete
315
315
  | Jira bug creation fails | Note failure, continue remaining bugs, report failures at end |
316
316
  | OTP screen appears unexpectedly | Type the `QA_OTP_CODE` value from `.env`, proceed |
317
317
  | Page requires further navigation after login | Follow redirect, confirm TARGET_URL loads |
318
+
319
+ ---
320
+
321
+ ## Final Step — Report to team dashboard (optional, silent)
322
+
323
+ Always the last action of the run, after everything else has printed. This is a no-op
324
+ unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
325
+ about it, never retry, never treat a non-zero exit as a run failure.
326
+
327
+ ```
328
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
329
+ --skill accessibility-testing --card {JIRA_CARD_ID, omit if none} \
330
+ --outcome {Pass|Fail|Partial} --bugs "{comma-separated bug IDs, or omit if none}"
331
+ ```
332
+ This skill has no `PRODUCT_FOLDER`; omit `--product` and the record is filed under the
333
+ calling repo's name automatically. Omit `--card` too when the run had no Jira card.
@@ -531,3 +531,21 @@ Print one line only (omit entirely if file not found):
531
531
  ```
532
532
  Selectors written to product context ({N} elements, {N} flows).
533
533
  ```
534
+
535
+ ---
536
+
537
+ ## Final Step — Report to team dashboard (optional, silent)
538
+
539
+ Always the last action of the run, after everything else has printed. This is a no-op
540
+ unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
541
+ about it, never retry, never treat a non-zero exit as a run failure.
542
+
543
+ ```
544
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
545
+ --skill automation --product {PRODUCT_FOLDER} --card {CARD_ID} \
546
+ --outcome {Pass|Fail|Partial} --reuse {reuse % number} --phase Automation
547
+ ```
548
+ `report-run` takes only short, structurally-constrained values (product folder, card ID,
549
+ skill name, outcome, bug IDs). It has no `--notes` flag — never inline freeform text such as
550
+ a finding, a page title, or a bug description (see "Never inline arbitrary text" in
551
+ `SKILLS_CONTEXT.md`).
@@ -293,4 +293,21 @@ Show a summary:
293
293
  - Notified: [Confirmed person's name]
294
294
  - Screenshots attached: [list of filenames, or "none" / "credentials not set"]
295
295
 
296
- ---
296
+ ---
297
+
298
+ ## Final Step — Report to team dashboard (optional, silent)
299
+
300
+ Always the last action of the run, after everything else has printed. This is a no-op
301
+ unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
302
+ about it, never retry, never treat a non-zero exit as a run failure.
303
+
304
+ ```
305
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
306
+ --skill bug-reporting --product {PRODUCT_FOLDER} --card {the card just filed/updated} \
307
+ --outcome {Pass if the bug posted, Fail if it did not} \
308
+ --bugs "{the bug key(s) filed}"
309
+ ```
310
+ `report-run` takes only short, structurally-constrained values (product folder, card ID,
311
+ skill name, outcome, bug IDs). It has no `--notes` flag — never inline freeform text such as
312
+ a finding, a page title, or a bug description (see "Never inline arbitrary text" in
313
+ `SKILLS_CONTEXT.md`).
@@ -108,4 +108,19 @@ Rules for each entry:
108
108
 
109
109
  ## Step 5 — Stop
110
110
 
111
- The report is the deliverable. Do NOT run any test, do NOT ask whether to run them — the QA reviews the report and runs files manually using the footer command. End the turn after printing the report.
111
+ The report is the deliverable. Do NOT run any test, do NOT ask whether to run them — the QA reviews the report and runs files manually using the footer command. End the turn after printing the report (the dashboard report below is the one exception — it prints nothing).
112
+
113
+ ---
114
+
115
+ ## Final Step — Report to team dashboard (optional, silent)
116
+
117
+ Run this after the report is printed and before ending the turn. This is a no-op
118
+ unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
119
+ about it, never retry, never treat a non-zero exit as a run failure.
120
+
121
+ ```
122
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
123
+ --skill impacted-tests --outcome Pass
124
+ ```
125
+ No product folder and no card in this skill — omit both flags; the record is filed under
126
+ the calling repo's name.
@@ -56,3 +56,17 @@ After it runs, tell the tester the next steps:
56
56
  - Keep real tokens out of version control (the scaffold's `.gitignore` covers `secrets.local.*`).
57
57
  - If the user wants the structure tailored (their user types, real endpoints from a Postman
58
58
  collection), scaffold first, then edit the generated templates to match.
59
+
60
+ ---
61
+
62
+ ## Final Step — Report to team dashboard (optional, silent)
63
+
64
+ Always the last action of the run, after everything else has printed. This is a no-op
65
+ unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
66
+ about it, never retry, never treat a non-zero exit as a run failure.
67
+
68
+ ```
69
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
70
+ --skill k6-framework-scaffold --outcome {Pass|Fail}
71
+ ```
72
+ No product folder and no card — omit both flags.
@@ -627,3 +627,22 @@ Do NOT stream: full charter text, all test results (save to file), full bug repo
627
627
  | Figma URL missing / user says no | Skip UI Testing, note in final summary |
628
628
  | `browser_evaluate` returns null for element | Log "element not captured" in hints, continue |
629
629
  | Bug creation fails | Note failure, continue with remaining bugs, report at end |
630
+
631
+ ---
632
+
633
+ ## Final Step — Report to team dashboard (optional, silent)
634
+
635
+ Always the last action of the run, after everything else has printed. This is a no-op
636
+ unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never print anything
637
+ about it, never retry, never treat a non-zero exit as a run failure.
638
+
639
+ ```
640
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
641
+ --skill manual-testing --product {PRODUCT_FOLDER} --card {CARD_ID} \
642
+ --outcome {Pass|Fail|Partial} --bugs "{comma-separated bug IDs, or omit if none}" \
643
+ --phase Manual
644
+ ```
645
+ `report-run` takes only short, structurally-constrained values (product folder, card ID,
646
+ skill name, outcome, bug IDs). It has no `--notes` flag — never inline freeform text such as
647
+ a finding, a page title, or a bug description (see "Never inline arbitrary text" in
648
+ `SKILLS_CONTEXT.md`).
@@ -591,3 +591,21 @@ One-line confirmation either way:
591
591
  ```
592
592
  Mobile context saved → .claude/skills/qa-agent/product_context/{PRODUCT_FOLDER}/context.md ({N} elements, {N} flows).
593
593
  ```
594
+
595
+ **Then report to the team dashboard (optional, silent)** — using the same values just
596
+ written to the Runs Log row:
597
+
598
+ ```
599
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
600
+ --skill mobile-automation --product {PRODUCT_FOLDER} --card {CARD_ID} \
601
+ --phase Automation \
602
+ --outcome {OUTCOME} --bugs "{comma-separated bug IDs or empty}" \
603
+ --reuse {reuse % number}
604
+ ```
605
+
606
+ Only pass `--product`/`--card`/`--phase`/`--outcome`/`--bugs`/`--reuse` — short,
607
+ structurally-constrained values (see "Never inline arbitrary text" in
608
+ `SKILLS_CONTEXT.md`). `report-run` has no `--notes` flag; don't inline the freeform
609
+ Runs Log note here. This is a no-op unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are
610
+ set in `.env` — never print anything about it, never retry, never treat a non-zero exit
611
+ as a run failure.
@@ -0,0 +1,365 @@
1
+ ---
2
+ name: mobilewright-setup
3
+ description: >
4
+ Scaffolds a complete BDD end-to-end test automation setup for a mobile app repo —
5
+ Cucumber.js (Gherkin feature files + step definitions), Mobilewright (a
6
+ Playwright-style device automation framework for iOS/Android, native or
7
+ React Native/Expo), and a "living documentation" HTML report generator
8
+ (multiple-cucumber-html-reporter). Works on any mobile app project: Expo,
9
+ bare React Native, native iOS (UIKit/SwiftUI), or native Android
10
+ (Views/Jetpack Compose) — it detects the project shape and adapts.
11
+ Use this skill when the user asks to "set up mobile testing", "add BDD /
12
+ Cucumber tests", "set up mobilewright", "scaffold e2e tests for the mobile
13
+ app", "add a living documentation / test report generator", "wire up
14
+ test:report / living-doc script", or generally wants end-to-end mobile UI
15
+ test automation added to a repo from scratch. Also use it as a reference
16
+ when adding new feature files or step definitions to a project that
17
+ already has this setup, so conventions stay consistent.
18
+ ---
19
+
20
+ # Mobile BDD Test Setup
21
+
22
+ Sets up a full BDD test-automation stack for a mobile app repo, end to end:
23
+ **Gherkin features → step definitions → Mobilewright device driver → Cucumber
24
+ JSON reports → an HTML "living documentation" report.** Everything is created
25
+ fresh in a single pass, adapted to whatever mobile project you're dropped into.
26
+
27
+ This skill does the scaffolding. It does not know the target app's screens —
28
+ budget time to explore the app's source (or a running build) to write the
29
+ first real feature/step files with correct locators.
30
+
31
+ ## 0. Mental model
32
+
33
+ - **Cucumber.js** runs `.feature` files (Gherkin) against step definitions in
34
+ `test/step-definitions/**/*.mjs`. It owns test discovery, execution, and
35
+ JSON report output.
36
+ - **Mobilewright** (`mobilewright` npm package) is the device driver —
37
+ Playwright's API (`getByRole`, `getByText`, `expect(...).toBeVisible()`,
38
+ auto-waiting) but pointed at a real/emulated/simulated mobile device
39
+ instead of a browser. One config file (`mobilewright.config.mjs`) declares
40
+ which platform/device/app to drive; Cucumber's `World` uses it to launch
41
+ the app before each scenario.
42
+ - **multiple-cucumber-html-reporter** turns the Cucumber JSON output into a
43
+ browsable "living documentation" HTML report — this is the `report` /
44
+ `living-doc.mjs` script.
45
+ - Nothing here is React-Native-specific. Mobilewright drives the OS
46
+ accessibility tree, so this setup works for native iOS/Android apps too —
47
+ only the locators change (see §6).
48
+
49
+ ## 1. Discover the target project
50
+
51
+ Before writing anything, gather facts. Do this with direct reads/greps, not
52
+ guesses:
53
+
54
+ 1. **Package manager & repo shape** — read `package.json`. Note the `name`
55
+ and `version` (used later for report branding). Check for `expo` in
56
+ dependencies (Expo/RN project) vs. presence of `ios/`/`android/` native
57
+ folders only (bare RN or native project).
58
+ 2. **Bundle / package identifier** — look in this order and stop at the
59
+ first match:
60
+ - `app.config.ts` / `app.config.js` / `app.json` (Expo) — read
61
+ `ios.bundleIdentifier` and `android.package`. These are sometimes
62
+ computed by a function (env-variant based, like a `getBundleIdentifier()`
63
+ helper) rather than a plain string — if so, read the function to find
64
+ the actual default value, don't just grab the string "getBundleIdentifier()".
65
+ - `ios/*/Info.plist` → `CFBundleIdentifier`, or `*.xcodeproj` → `PRODUCT_BUNDLE_IDENTIFIER`.
66
+ - `android/app/build.gradle` (or `.kts`) → `applicationId`.
67
+ - If genuinely ambiguous (multiple variants/flavors: dev/qa/preprod/prod),
68
+ ask the user which build variant to test against — don't silently pick one.
69
+ 3. **Existing test setup** — check for `test/`, `e2e/`, `cucumber.js`,
70
+ `*.feature` files, or an existing `mobilewright.config.*`. If any of this
71
+ already exists, this is an **update**, not a fresh scaffold: read what's
72
+ there and extend it rather than overwriting. Never blindly clobber an
73
+ existing `mobilewright.config.*`, `cucumber.js`, or `world.mjs`.
74
+ 4. **Testing platform** — infer if possible (e.g. only `android/` present →
75
+ android; only `ios/` → ios). If both exist and the user didn't say, ask.
76
+
77
+ Only ask the user (via a clarifying question) for facts you could not
78
+ determine from the repo: target **platform** (`ios`/`android`), a
79
+ **device** to run against (a specific simulator/emulator name, or "use
80
+ auto-discovery"), and the **bundle/app id** if it's genuinely ambiguous.
81
+ Everything else, infer and proceed — this skill runs under an
82
+ auto/no-interruption workflow by default, so don't stop for things you can
83
+ reasonably derive.
84
+
85
+ ## 2. Install dependencies
86
+
87
+ Detect the package manager from the lockfile present (`package-lock.json` →
88
+ npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, `bun.lockb` → bun) and
89
+ install as **devDependencies**:
90
+
91
+ ```bash
92
+ npm install -D @cucumber/cucumber mobilewright multiple-cucumber-html-reporter
93
+ ```
94
+
95
+ (swap `npm install -D` for the equivalent `yarn add -D` / `pnpm add -D` /
96
+ `bun add -d` as appropriate). Do not add these as runtime `dependencies`.
97
+
98
+ ## 3. Create `mobilewright.config.mjs`
99
+
100
+ Write to the project root. Fill in the platform/bundleId/device values
101
+ discovered or confirmed in step 1 — never leave placeholder text in the
102
+ committed file.
103
+
104
+ ```javascript
105
+ import { defineConfig } from 'mobilewright';
106
+
107
+ export default defineConfig({
108
+ platform: '{{PLATFORM}}', // 'android' | 'ios'
109
+ bundleId: '{{BUNDLE_ID}}', // e.g. com.example.myapp(.qa)
110
+ deviceId: '{{DEVICE_ID}}', // named emulator/simulator profile, if the user gave one — omit this line if using auto-discovery instead
111
+ deviceType: '{{DEVICE_TYPE}}', // 'emulator' | 'simulator' | 'real-device' — omit if unknown
112
+ timeout: 30_000,
113
+ });
114
+ ```
115
+
116
+ Notes:
117
+ - `deviceId`/`deviceType` are optional — Mobilewright auto-discovers the
118
+ first booted device matching `platform` if omitted. Prefer auto-discovery
119
+ unless the user specifically named a device/profile.
120
+ - If the project already targets both platforms via CI, consider the
121
+ `projects` array (Mobilewright's multi-device/platform matrix — see the
122
+ `projects` option in the mobilewright package's own `README.md` under
123
+ `node_modules/mobilewright/README.md`, or https://mobilewright.dev) instead
124
+ of a single flat config. Only do this if the user asks for multi-platform
125
+ runs; default to a single flat config otherwise.
126
+ - A one-line comment noting *why* a value was picked (e.g. "QA build shared
127
+ for testing") is fine and matches existing convention, but keep it to one
128
+ line.
129
+
130
+ ## 4. Create `cucumber.js`
131
+
132
+ Root-level Cucumber profile. This wires feature discovery, step import, and
133
+ report output:
134
+
135
+ ```javascript
136
+ module.exports = {
137
+ default: [
138
+ 'test/features/**/*.feature',
139
+ '--import test/step-definitions/**/*.mjs',
140
+ `--format json:reports/cucumber-report-${Date.now()}.json`,
141
+ '--format summary',
142
+ '--format progress-bar',
143
+ ].join(' '),
144
+ };
145
+ ```
146
+
147
+ Keep the feature/step-definition globs as-is unless the target repo already
148
+ uses a different test root (e.g. `e2e/` instead of `test/`) — match whatever
149
+ already exists rather than introducing a second convention.
150
+
151
+ ## 5. Create `test/step-definitions/support/world.mjs`
152
+
153
+ This is the Cucumber `World` — it owns the device lifecycle (launch before
154
+ each scenario, close after). This file is intentionally app-agnostic; do not
155
+ add app-specific logic here.
156
+
157
+ ```javascript
158
+ import { setWorldConstructor, setDefaultTimeout, Before, After } from '@cucumber/cucumber';
159
+ import { ios, android } from 'mobilewright';
160
+ import config from '../../../mobilewright.config.mjs';
161
+
162
+ setDefaultTimeout(config.timeout ?? 30_000);
163
+
164
+ class MobileWorld {
165
+ async launch() {
166
+ const launcher = config.platform === 'android' ? android : ios;
167
+ this.device = await launcher.launch({
168
+ bundleId: config.bundleId,
169
+ deviceId: config.deviceId,
170
+ deviceName: config.deviceName,
171
+ installApps: config.installApps,
172
+ });
173
+ this.screen = this.device.screen;
174
+ }
175
+
176
+ async teardown() {
177
+ await this.device?.close();
178
+ }
179
+ }
180
+
181
+ setWorldConstructor(MobileWorld);
182
+
183
+ Before(async function () {
184
+ await this.launch();
185
+ });
186
+
187
+ After(async function () {
188
+ await this.teardown();
189
+ });
190
+ ```
191
+
192
+ Every step definition gets `this.device` and `this.screen` from this World —
193
+ step files never construct their own device connection.
194
+
195
+ ## 6. Write the first feature + step definitions
196
+
197
+ Don't hand-wave this — a setup with zero real assertions isn't done. Find one
198
+ real, stable entry point in the app under test and write one smoke scenario
199
+ against it:
200
+
201
+ 1. **Find a locator.** Grep the app source for existing test hooks in
202
+ priority order (this matches Mobilewright's own locator priority:
203
+ `getByTestId` > `getByRole` > `getByLabel` > `getByText`):
204
+ - React Native/Expo: search for `testID=`, `accessibilityLabel=`,
205
+ `accessibilityRole=` props on the first/home screen's root and key
206
+ interactive elements.
207
+ - Native iOS: `accessibilityIdentifier` / `accessibilityLabel` in
208
+ SwiftUI/UIKit source.
209
+ - Native Android: `contentDescription` / view `id`s, or Compose
210
+ `Modifier.testTag(...)` / `semantics { }`.
211
+ - If nothing is tagged yet, don't invent testIDs blindly — either add a
212
+ minimal `testID`/`accessibilityIdentifier` to the one element you need
213
+ (small, targeted, and tell the user you did it), or fall back to
214
+ `getByText('...')` against real on-screen copy you found in source.
215
+ 2. **If you can run a booted simulator/emulator**, `npx mobilewright inspect`
216
+ opens a live element inspector against the connected device — use it to
217
+ confirm real locators instead of guessing from source alone.
218
+ 3. Write `test/features/<area>.feature`:
219
+
220
+ ```gherkin
221
+ Feature: <Screen or flow name>
222
+
223
+ Scenario: <one concrete, observable outcome>
224
+ Given <a starting state, e.g. "the app has launched">
225
+ Then <an assertion tied to a real locator>
226
+ ```
227
+
228
+ 4. Write the matching `test/step-definitions/<area>.steps.mjs`:
229
+
230
+ ```javascript
231
+ import { Given, When, Then } from '@cucumber/cucumber';
232
+ import { expect } from 'mobilewright';
233
+
234
+ Given('the app has launched', async function () {
235
+ await expect(this.screen.getByTestId('<real-test-id>')).toBeVisible();
236
+ });
237
+ ```
238
+
239
+ Step conventions to follow:
240
+ - One `Given`/`When`/`Then` per file topic; keep step text reusable across
241
+ scenarios in the same feature area rather than one-off phrasing per
242
+ scenario.
243
+ - Prefer `getByTestId` / `getByRole` over `getByText`/coordinate taps —
244
+ they're the least brittle across locale and copy changes.
245
+ - Never hardcode secrets (passwords, tokens) directly in `.feature` files if
246
+ the repo has an env/secrets convention already (`.env`, CI variable
247
+ groups, etc.) — follow that repo's existing pattern instead. If unsure, ask.
248
+
249
+ ## 7. Create `scripts/living-doc.mjs`
250
+
251
+ Generates the browsable HTML report from the Cucumber JSON output. Pull
252
+ branding from `package.json` — never hardcode a project name:
253
+
254
+ ```javascript
255
+ import { readFileSync } from 'fs';
256
+ import { generate } from 'multiple-cucumber-html-reporter';
257
+ import config from '../mobilewright.config.mjs';
258
+
259
+ const { name, version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)));
260
+
261
+ const displayName = '{{DISPLAY_NAME}}'; // human-readable project name, e.g. derived from package.json `name`
262
+
263
+ generate({
264
+ jsonDir: 'reports/',
265
+ reportPath: 'living-documentation/',
266
+ pageTitle: displayName,
267
+ pageFooter: `<div class="created-by"><p>${displayName} QA</p></div>`,
268
+ reportName: displayName,
269
+ metadata: {
270
+ device: config.deviceName ?? config.deviceId,
271
+ platform: { name: config.platform === 'ios' ? 'iOS' : 'Android', version: '' },
272
+ app: { name, version },
273
+ },
274
+ customData: {
275
+ title: 'Run info',
276
+ data: [{ label: 'Project', value: name }],
277
+ },
278
+ });
279
+ ```
280
+
281
+ Derive `displayName` from `package.json`'s `name` (title-cased, hyphens/
282
+ underscores replaced with spaces) unless the repo already has an obvious
283
+ human-readable product name elsewhere (README title, app display name in
284
+ `app.config.*`/`app.json`) — prefer that if present.
285
+
286
+ ## 8. Wire up `package.json` scripts
287
+
288
+ Add (don't replace unrelated existing scripts):
289
+
290
+ ```json
291
+ {
292
+ "scripts": {
293
+ "test": "cucumber-js",
294
+ "test:doctor": "mobilewright doctor",
295
+ "report": "node scripts/living-doc.mjs",
296
+ "test:report": "npm run test && npm run report"
297
+ }
298
+ }
299
+ ```
300
+
301
+ If a `test` script already exists for a different test runner (unit tests,
302
+ etc.), don't clobber it — use a distinct name instead
303
+ (`test:e2e`/`test:bdd`/`test:mobile`) and say so to the user, then adjust
304
+ `test:report` to reference the new name.
305
+
306
+ ## 9. Update `.gitignore`
307
+
308
+ Ensure generated test artifacts aren't committed. Add if missing:
309
+
310
+ ```
311
+ # cucumber JSON reports + generated living documentation
312
+ reports
313
+ living-documentation
314
+
315
+ # test app binaries (built artifacts, not source)
316
+ test/app/
317
+ ```
318
+
319
+ Only add the `test/app/` line if the repo's test setup installs app binaries
320
+ locally (check whether `installApps` is used in the config); skip it
321
+ otherwise.
322
+
323
+ ## 10. Verify the environment
324
+
325
+ Run the doctor check and report its output to the user — don't just assume
326
+ success:
327
+
328
+ ```bash
329
+ npx mobilewright doctor
330
+ ```
331
+
332
+ This validates Xcode/Android SDK/emulator-simulator availability and tells
333
+ you exactly what's missing. Surface any failures verbatim; don't try to
334
+ silently work around a missing SDK/toolchain.
335
+
336
+ If a device is booted and reachable, also do a real run to confirm the
337
+ scaffold works end to end:
338
+
339
+ ```bash
340
+ npm run test:report
341
+ ```
342
+
343
+ ## 11. Summary to the user
344
+
345
+ After setup, tell the user concretely:
346
+ - What was installed and created (file list).
347
+ - Which platform/device/bundle id the config targets, and why (inferred vs.
348
+ user-provided).
349
+ - The doctor check result.
350
+ - How to run tests day to day: `npm test` (or the chosen script name),
351
+ `npm run report` / `npm run test:report`, and where the living-doc report
352
+ lands (`living-documentation/index.html`).
353
+ - That new features go in `test/features/*.feature` with matching
354
+ `test/step-definitions/*.steps.mjs`, and that `world.mjs` under
355
+ `test/step-definitions/support/` should stay app-agnostic — device
356
+ lifecycle only, no scenario-specific logic.
357
+
358
+ ## Reference
359
+
360
+ Full Mobilewright API (locators, assertions, config options, CLI commands)
361
+ is documented in the installed package at `node_modules/mobilewright/README.md`
362
+ and `node_modules/mobilewright/reference.md` once installed — read those
363
+ directly rather than relying on memory when writing non-trivial step
364
+ definitions (e.g. WebView bridging, multi-device `projects` config, role
365
+ mapping tables).
@@ -370,6 +370,21 @@ Write `context.md`, then print one line:
370
370
  Product context saved → .claude/skills/qa-agent/product_context/{PRODUCT_FOLDER}/context.md
371
371
  ```
372
372
 
373
+ ### 6f — Report to team dashboard (optional, silent)
374
+
375
+ Immediately after 6e, using the same values just written to the Runs Log row, shell out:
376
+
377
+ ```
378
+ node .claude/skills/qa-agent/toolkit/qa-toolkit.cjs report-run \
379
+ --skill qa-agent --product {PRODUCT_FOLDER} --card {CARD_ID} \
380
+ --phase {Manual|Automation|Both} \
381
+ --outcome {OUTCOME} --bugs "{comma-separated bug IDs or empty}" \
382
+ --reuse {reuse % number, omit for manual-only runs}
383
+ ```
384
+
385
+ This is a no-op unless `QA_DASHBOARD_REPO`/`QA_DASHBOARD_TOKEN` are set in `.env` — never
386
+ print anything about it, never retry, never treat a non-zero exit as a run failure.
387
+
373
388
  ---
374
389
 
375
390
  ## Post-run Cleanup (all phases)
@@ -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;
@@ -719,6 +743,109 @@ function cmdRoi(args) {
719
743
  );
720
744
  }
721
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
+
722
849
  // ── dispatch ────────────────────────────────────────────────────────────
723
850
 
724
851
  function main() {
@@ -741,6 +868,7 @@ function main() {
741
868
  dashboard: cmdDashboard,
742
869
  digest: cmdDigest,
743
870
  roi: cmdRoi,
871
+ 'report-run': cmdReportRun,
744
872
  };
745
873
  const handler = handlers[command];
746
874
  if (!handler) {
@@ -748,11 +876,11 @@ function main() {
748
876
  'Usage: qa-toolkit.cjs <command> [--flags]\n' +
749
877
  'Commands: list-products, get-bugs, get-runs, get-selectors, fingerprint, pii-scan, dup-bug,\n' +
750
878
  ' cost-estimate, risk-score, trust-record, trust-status,\n' +
751
- ' locator-record, locator-query, dashboard, digest, roi'
879
+ ' locator-record, locator-query, dashboard, digest, roi, report-run'
752
880
  );
753
881
  process.exit(command ? 1 : 0);
754
882
  }
755
- handler(args);
883
+ Promise.resolve(handler(args)).catch(() => {});
756
884
  }
757
885
 
758
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,9 +1,10 @@
1
1
  {
2
2
  "name": "@roopesh.yadava/qa-pack",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
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
- "postinstall": "node bin/postinstall.js"
6
+ "postinstall": "node bin/postinstall.js",
7
+ "test": "node --test test/"
7
8
  },
8
9
  "files": [
9
10
  "bin/",
@@ -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=