Dentolize · Composable Sandbox QA Walkthrough
On this pageTermsData shapesLabelsRelated workflows

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-composableci-qa-composable. Introduced in this PR to fix artifact-name failures on branches containing / (:247).

State directoryQA_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:

FieldRequiredUsed atNotes
idYes:174, :177, :185, :193Must match /^QA-\d+$/ for fix to re-find the issue (:167). Entries without an id are skipped entirely.
titleEffectively:185, :193Goes into the issue title after the ID and emoji.
resultEffectively:175, :176, :229One of PASS/FAIL/FIXED/SKIPPED; anything else renders ❔ and is excluded from the tally.
resultDetailNo:181, :197Free markdown; the evidence body.
storyNo:197The scenario text.
roleNo:195Renders as role \receptionist\`; defaults to ?`.
priorityNo:195, :199critical 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.

OutputTypeRead at
QA_REVIEWfile path:126
QA_PLANfile path:126
QA_STORIESfile path:153
QA_ISSUESfile path:210
QA_SUMMARYfile path:226
QA_STATE_DIRdirectory path:242, :248
QA_SLUGstring: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)

VariableValue
GITHUB_TOKENgithub.token — the agent acts with the workflow's permissions
QA_SCOPEall 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

LabelColourCreated by workflow?Applied to
ai-automated8B5CF6 (purple)Yes (:145)Every issue the agent files
pr-<number>0E8A16 (green)Yes (:146)Every issue for that PR
branch-<name>0E8A16YesUsed instead of pr-N when the branch has no open PR; truncated to 50 chars (:143)
criticalNoStories 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.


FileTriggerPurpose
.github/workflows/sandbox.ymlPR opened / synchronized / closedDeploys and destroys the per-PR sandbox; posts the credentials comment; kicks off docs generation
.github/workflows/sandbox-ops.ymlManual dispatchreseed, reset-data, redeploy, destroy, update-docs on an existing sandbox
.github/workflows/sandbox-qa.ymlManual dispatchThis 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.