Glossary
Terms, data shapes, and where each one lives in the code. File references are to .github/workflows/sandbox-qa.yml unless stated otherwise.
Terms
Action — One of the five things the QA workflow can do: review, plan, test, fix, full-auto. Chosen from a dropdown at dispatch. Replaces the old mode input (:26-36).
Agent — The AI that does the actual reviewing, planning, testing and fixing. Runs as headless Claude Code inside the sandbox-qa-agent container on the homelab host, invoked by the workflow at :93. Its code is not in this repository.
Artifact — The archive attached to each workflow run: agent log, plan, review, raw results. Named sandbox-qa-<slug>-<run_id>, retained 14 days (:241-250).
Composable — The property this PR adds: each action is independently dispatchable, produces a durable artifact, and consumes the previous action's (possibly human-edited) artifact.
Dispatch — A manual run of the workflow. This workflow has only a workflow_dispatch trigger (:23-24) — it never runs automatically on push or PR.
Editable handoff — The mechanism by which a human corrects the AI between stages: the AI posts a PR comment, the human edits it, the next action reads the comment's current body. See Editable handoffs.
Full-auto — The unattended action that chains all four stages. Notably does not read human-edited comments (:61).
Marker — An invisible HTML comment at the top of each AI-managed PR comment, used to find and update it: <!-- sandbox-qa-review -->, <!-- sandbox-qa-plan -->, <!-- sandbox-qa-report -->. Deleting a marker while editing causes the next run to post a duplicate comment instead of updating yours.
Mode — The old name for action, removed in this PR. Survives only as a dead alias at :101 and in a stale code comment at :158-159. See Migration.
Result — A story's outcome: PASS, FAIL, FIXED, SKIPPED (:152).
Sandbox — The isolated per-branch deployment of the whole Dentolize stack that the agent tests against, created by sandbox.yml when a PR opens and destroyed when it closes. Reachable at <slug>.sandbox.anastawfik.com.
Scope — A secondary input, all or failed-only, meaningful only for the test action. Passed to the agent as the QA_SCOPE environment variable, not as an argument (:37-44, :88).
Slug — A filesystem- and URL-safe form of the branch name, emitted by the agent script as QA_SLUG. ci/qa-composable → ci-qa-composable. Introduced in this PR to fix artifact-name failures on branches containing / (:247).
State directory — QA_STATE_DIR: the agent's per-branch working directory on the host, persisting across dispatches. This persistence is what lets plan build on a review from an earlier job. Replaces the per-run QA_RUN_DIR (:242, :248).
Story — One user-story test scenario: a scripted clinic workflow performed as a specific role, with an expected outcome. The unit of both planning and reporting — one story in, one GitHub issue out.
Upsert — Create-or-update. Every AI comment is upserted, so each stage owns exactly one comment that is overwritten on each run; no history accumulates on the PR (:114-122).
Data shapes
These are contracts between the agent script and the workflow. Neither side is validated by a schema — the workflow reads defensively and degrades on mismatch.
stories.json — read from QA_STORIES (:153-157)
An array of story objects. Fields the workflow actually uses:
| Field | Required | Used at | Notes |
|---|---|---|---|
id | Yes | :174, :177, :185, :193 | Must match /^QA-\d+$/ for fix to re-find the issue (:167). Entries without an id are skipped entirely. |
title | Effectively | :185, :193 | Goes into the issue title after the ID and emoji. |
result | Effectively | :175, :176, :229 | One of PASS/FAIL/FIXED/SKIPPED; anything else renders ❔ and is excluded from the tally. |
resultDetail | No | :181, :197 | Free markdown; the evidence body. |
story | No | :197 | The scenario text. |
role | No | :195 | Renders as role \receptionist\`; defaults to ?`. |
priority | No | :195, :199 | critical adds the critical label — which the workflow does not create, unlike ai-automated and pr-N (:144-150). |
An unparsable file produces core.warning and a green run reporting zero stories (:156).
issues.json — read from QA_ISSUES (:210-223)
An array of technical findings not tied to a story. Fields: title (required — entries without it are skipped), body, severity (critical adds the critical label). Issues are titled [QA] <title>, always created open, and never matched against previous runs — so they duplicate on every reporting run.
Step outputs from the agent (:93)
Emitted as KEY=VALUE lines on stdout, redirected into $GITHUB_OUTPUT.
| Output | Type | Read at |
|---|---|---|
QA_REVIEW | file path | :126 |
QA_PLAN | file path | :126 |
QA_STORIES | file path | :153 |
QA_ISSUES | file path | :210 |
QA_SUMMARY | file path | :226 |
QA_STATE_DIR | directory path | :242, :248 |
QA_SLUG | string | :247 |
Because stdout is the machine channel, the agent's human-readable progress must go to stderr — which is where the "live activity streaming in the job log" the PR mentions appears.
Environment passed into the agent (:86-90)
| Variable | Value |
|---|---|
GITHUB_TOKEN | github.token — the agent acts with the workflow's permissions |
QA_SCOPE | all or failed-only |
QA_REVIEW_FILE | $RUNNER_TEMP/qa-review.md — may not exist |
QA_PLAN_FILE | $RUNNER_TEMP/qa-plan.md — may not exist |
Both file paths are exported unconditionally, including for actions where the fetch step never ran (review, fix, full-auto, per :61). The agent must handle their absence.
Labels
| Label | Colour | Created by workflow? | Applied to |
|---|---|---|---|
ai-automated | 8B5CF6 (purple) | Yes (:145) | Every issue the agent files |
pr-<number> | 0E8A16 (green) | Yes (:146) | Every issue for that PR |
branch-<name> | 0E8A16 | Yes | Used instead of pr-N when the branch has no open PR; truncated to 50 chars (:143) |
critical | — | No | Stories and findings with priority/severity of critical (:199, :219) |
The critical gap is the one to watch: if that label does not already exist in the repository, issues.create throws and the publish step fails partway through, after some issues have already been filed.
Related workflows
| File | Trigger | Purpose |
|---|---|---|
.github/workflows/sandbox.yml | PR opened / synchronized / closed | Deploys and destroys the per-PR sandbox; posts the credentials comment; kicks off docs generation |
.github/workflows/sandbox-ops.yml | Manual dispatch | reseed, reset-data, redeploy, destroy, update-docs on an existing sandbox |
.github/workflows/sandbox-qa.yml | Manual dispatch | This PR. Runs the AI QA agent against a sandbox |
All three run on the same self-hosted homelab runner ([self-hosted, homelab, dentolize]) with a single slot, and none of them use actions/checkout — the sandbox CLI manages its own shared clone tree. A long QA run therefore blocks deploys and ops for every branch.