The five actions
Business view
The Sandbox QA workflow is dispatched by hand from the repository's Actions tab, on the branch you want examined. You pick an action from a dropdown and, if you picked test, a scope.
Think of the four core actions as a relay where you can stop the runner between each leg, read what they wrote on the baton, and cross bits out.
review
"Tell me what you think this change does."
The agent reads the branch's code changes and writes two kinds of assessment — technical (is this correct, safe, consistent with the codebase) and business (what does this mean for a clinic actually using it). The output lands as a single PR comment.
Nothing is tested. No browser is opened. No code is touched. This is the cheapest way to find out whether the AI understood the change at all — and it is the artifact that was previously invisible.
plan
"Turn that understanding into a list of things to try."
The agent writes user stories: concrete scenarios, each tagged with the clinic role that would perform it (owner, receptionist, doctor, accountant) and a priority. It builds these on the review — including any edits you made to the review comment.
The plan lands as its own PR comment, also editable. Still nothing tested.
test
"Do the things on the list."
The agent drives a real browser against the branch's live sandbox environment, walking each story. Every story gets a result — PASS, FAIL, FIXED, or SKIPPED — and every story becomes a GitHub issue. A summary comment lands on the PR with the tally.
test never commits code. This is stated in the workflow header (.github/workflows/sandbox-qa.yml:9-10) and is true of the GitHub-side logic: the publish step only creates comments and issues.
test is the dropdown default (.github/workflows/sandbox-qa.yml:30), which is the safe choice — the default button does not write to your branch.
The scope dropdown
scope: all runs every story. scope: failed-only re-runs only the stories that failed last time — useful after you have hand-fixed something and want a quick confirmation rather than another full pass.
fix
"Repair what failed, then check your work."
fix is the only action besides full-auto that pushes commits to the PR branch. It takes the previous test's failures, attempts repairs, re-tests, and then goes back to the issues it filed earlier and updates them in place — commenting the re-test result, retitling with the new status emoji, and closing the ones that now pass.
Fix commits are marked [skip ci] and the agent redeploys the sandbox itself, because the homelab runner has a single slot and cannot run a deploy job while the QA job is holding it (.github/workflows/sandbox-qa.yml:16-19).
full-auto
All four, one dispatch, no stopping. This is the old one-shot behavior, kept for when you want to fire and forget. You give up every editing checkpoint by choosing it.
Technical view
The dispatch inputs
on:
workflow_dispatch:
inputs:
action:
description: QA action to run
type: choice
required: true
default: test
options:
- review
- plan
- test
- fix
- full-auto
scope:
description: Test scope (test action only)
type: choice
required: true
default: all
options:
- all
- failed-only
— .github/workflows/sandbox-qa.yml:23-44
The input was renamed mode → action and every option value changed (test-only → test, fix-issues → fix, plan-only → plan, execute → absorbed into test, new review). Because these are workflow_dispatch inputs, the rename is not backward compatible: any bookmark, script, or gh workflow run invocation passing -f mode=test-only will now fail. See Migration.
scope is declared required: true with a default, so GitHub renders it on every dispatch — including review, plan, and fix, where the description itself says it does not apply. It is a cosmetic wart, not a bug: the value is handed to the agent regardless and the agent is expected to ignore it outside test.
How each action reaches the agent
There is exactly one invocation and it does not branch:
env:
GITHUB_TOKEN: ${{ github.token }}
QA_SCOPE: ${{ inputs.scope }}
QA_REVIEW_FILE: ${{ runner.temp }}/qa-review.md
QA_PLAN_FILE: ${{ runner.temp }}/qa-plan.md
run: |
set -e
/opt/homelab/sandbox/bin/sandbox-qa dentolize "$GITHUB_REF_NAME" "${{ inputs.action }}" >> "$GITHUB_OUTPUT"
— .github/workflows/sandbox-qa.yml:84-93
Two things to notice.
scope travels by environment variable, not argument. The action is positional argument three; scope is only QA_SCOPE in the environment (:88). The script must read the environment to honour failed-only. Nothing in this repository verifies that it does.
Stdout is the output channel. >> "$GITHUB_OUTPUT" means every line the script prints to stdout is parsed by the runner as a KEY=VALUE workflow output. Any stray stdout line that isn't KEY=VALUE is either ignored or, if it contains an =, becomes a junk output. This is why the PR's "live agent activity streaming in the job log" must be written to stderr — and it is why that feature, though real in the runner's log, is not visible anywhere in this diff.
Per-branch state
The workflow itself is stateless — it checks nothing out (no actions/checkout, matching the sibling sandbox.yml and sandbox-ops.yml) and keeps nothing between runs. Continuity comes from two places:
- The PR comments, re-read at the start of
planandtest
(.github/workflows/sandbox-qa.yml:60-82).
QA_STATE_DIRon the host, a per-branch directory the script maintains
and the workflow uploads as an artifact (.github/workflows/sandbox-qa.yml:242-248).
The QA_RUN_DIR → QA_STATE_DIR rename is the visible edge of that shift: a run directory belongs to one job; a state directory belongs to the branch and outlives every dispatch. That is what lets plan build on a review from a separate job hours earlier, and what lets fix know which stories failed.
Which actions fetch your edits
- name: Fetch edited review / plan comments
if: inputs.action == 'plan' || inputs.action == 'test'
— .github/workflows/sandbox-qa.yml:60-61
Only plan and test. review has nothing upstream to read, which is correct. But full-auto and fix are also excluded, and that has a consequence worth knowing: full-auto will not pick up review or plan comments you have already edited. It regenerates everything from scratch in-run. If you have curated a plan and then dispatch full-auto expecting it to be used, it will not be. Use test for that.
fix not fetching is benign — it works from the stored test results, not from the plan text.
Action → behavior matrix, as implemented
fetches edits (:61) | posts editable comment (:125) | files/updates issues (:143-223) | posts summary (:230) | can push commits | |
|---|---|---|---|---|---|
review | no | yes (sandbox-qa-review) | no — returns at :139 | no | no |
plan | yes | yes (sandbox-qa-plan) | no — returns at :139 | no | no |
test | yes | no | creates new | yes | no |
fix | no | no | updates existing (:161) | yes | yes |
full-auto | no | no | creates new | yes | yes |
The return at .github/workflows/sandbox-qa.yml:139 is what makes review and plan cheap: they exit the publish script before any issue machinery runs.