@abiezon/qagent
v0.12.1
Published
Agentic QA hub — spec-tool agnostic
Maintainers
Readme
qagent
Agentic QA hub — agnostic to the spec tool. Personal project, not tied to any team.
It runs with spechelix or the ia-cgr that
preceded it, with OpenSpec, with a convention
of the target repo's own, or on its own — with none of the above. The hub never
assumes specs exist, nor that they follow a single format: every playbook
starts by resolving where the acceptance criterion is before doing any QA
work.
Instructions in this hub are written in English so the guidance stays consistent across models and tools. You can talk to the agent in any language — it replies in the one you use. See the language rule in
guardrails/qa-quality.md.
Source modes
| Mode | How it is detected | What it reads | Default QA_DIR |
|---|---|---|---|
| spechelix | Target repo has .ai/features/ (declared) | HU-XXX/FEAT-XXX/FIX-XXX/QA-HU-XXX, via INDEX.md | .ai/qa |
| ia-cgr | Target repo has .ai/features/ (declared) | FEAT-XXX.md/FIX-XXX.md | .ai/qa |
| OpenSpec | Target repo has openspec/ | proposal.md, specs/, tasks.md | .qa |
| custom | Target repo declares its own convention (in AGENTS.md/README) | Whatever the convention points to | .qa |
| standalone | None of the above | Description + acceptance criterion straight from the chat | .qa |
Full detail in
playbooks/source-resolution.md. The table
above, the aliases the CLI accepts, and the interactive menu all come from
sources.yaml — see Generation.
Quick start
Requires Node.js ≥ 18. Without installing anything:
npx @abiezon/qagent install --mode spechelixOr installed, for recurring use:
npm i -D @abiezon/qagentqagent install is the recommended entry point: it inspects the topology
before writing anything, because the two common arrangements need different
treatment.
Standalone repository — the path is itself a git repo. It instruments that:
$ npx qagent install --mode spechelix
🧭 qagent — standalone repository: /path/my-app
declaring SOURCE_MODE=spechelix
📋 .qagent/config.yaml: SOURCE_MODE=spechelix
📁 .qagent/config.yaml: QA_DIR=.ai/qa (default for spechelix)
📂 .ai/qa/ created — test plans, coverage reports and run reports land here
✅ AGENTS.md: pointer block updated (mode: spechelix)Workspace — an aggregating root that contains repos without being one.
The root is not a target: writing config there would create a .qagent/
no playbook reads, since they all resolve the source from the target repo.
install lists what it found, then asks which repo to instrument — or all of
them — and walks through the menu for the answer:
$ npx qagent install my-workspace
🧭 qagent — workspace: /path/my-workspace
the root is not a target; 2 product repo(s) detected inside it:
· my-app
· my-api
(skipped as tooling, not targets: spechelix —
point at the path explicitly if you want to instrument one of them anyway)
> Which repo should be instrumented?
1) my-app
2) my-api
a) all 2 of them
> aWith a single repo inside, there is nothing to choose and it goes straight to
the questions. In a pipe or a CI job nothing is asked: pass --mode (with
--all, or the path of one repo) and the listing tells you the exact command.
Repos that are tooling, not targets drop out of the listing by two
complementary checks: the directory name (qagent, spechelix, ia-cgr,
openspec — the tool_repos list in sources.yaml, compared
case-insensitively) and the agentic-hub shape (playbooks/ + guardrails/ at
the root), which catches a hub cloned under a different name. Neither is a
guarantee, which is why they only filter the listing: pointing at the path
explicitly still instruments, for whoever wants QA on the hub's own code.
qagent init still exists for explicit per-repo use, and refuses a workspace
root, pointing at install.
The menu
qagent init with no --mode, and qagent install with none — on a standalone
repo or on a workspace — walk through the whole setup instead of asking one
question and leaving the rest to flags nobody finds in --help. On a workspace
it first asks which repo to instrument, or all of them, which is what --all
and naming a path would otherwise require rerunning the command to say:
$ npx qagent init
> Which acceptance-criterion source does this repo use?
1) OpenSpec — openspec/ exists, or will exist
2) spechelix — Spechelix SDD hub, .ai/features/ exists or will exist
3) ia-cgr — SDD hub that preceded Spechelix, same .ai/features/ layout
4) manual/AI — no spec tool; qagent alone
> 2
> Where should QA artifacts be written (test plans, reports)?
enter to accept, or a path relative to the repo root
[.ai/qa] >
> Learning loop: each run reads what earlier ones recorded about
this project, and writes back what it learned.
enable it? [y/N] y
> Test credentials directory: a git-ignored place for the logins a
QA run needs. Test/dev environments only — never production.
create it? [y/N] yEvery question offers what the repo already has, or the default — including the
mode, so a repo instrumented by an earlier version keeps it by pressing
enter. That makes the menu the natural way to adopt what a new release added:
run it, hold enter, and the repo gains the directories the version created
without changing a single answer it already had. Re-running never resets
anything, and when several repos are instrumented at once, an accepted default
leaves each one's own QA_DIR alone instead of forcing one path onto all. In a pipe or a CI job
there is nothing to ask, so the menu never opens: pass --mode (and the flags
below), or status reports and exits.
Declaring the mode explicitly avoids a new repo (one that will gain
.ai/features/ or openspec/ later) being treated as standalone just
because those folders do not exist yet.
$ npx qagent init --mode spechelix
🧭 qagent — declaring SOURCE_MODE=spechelix
📋 .qagent/config.yaml: SOURCE_MODE=spechelix
📁 .qagent/config.yaml: QA_DIR=.ai/qa (default for spechelix)
📂 .ai/qa/ created — test plans, coverage reports and run reports land here
✅ AGENTS.md: pointer block updated (mode: spechelix)
ℹ️ .ai/features/ does not exist yet — normal if you are setting spechelix up laterThat writes in the target repo (here, my-app/):
.qagent/config.yaml
SOURCE_MODE: spechelix
QA_DIR: .ai/qa<QA_DIR>/README.md — a seed explaining what lands in the directory, so
whoever finds it in a diff months from now knows what it is. Written only if
absent; your edits are never overwritten. It also makes the directory
committable, since git tracks files and not folders.
AGENTS.md — a managed block, inserted right after the title; if the file
already has human content, the rest is preserved:
# my-app
<!-- qagent:pointer -->
> QA hub: [qagent](../qagent) · source mode: `spechelix` — see `../qagent/playbooks/source-resolution.md`
<!-- /qagent:pointer -->status confirms without redoing the work, and reports which hub version is
answering:
$ npx qagent status
🧭 @abiezon/qagent 0.7.0 (package)
📦 my-app: SOURCE_MODE=spechelix
📁 QA_DIR=.ai/qa
🧠 KNOWLEDGE=true (.ai/qa/knowledge/, 4 file(s))
🔐 CREDENTIALS=true (.qagent/credentials/)
🎯 dimensions always in scope: Functional, Regression, Security
evidence found: Compatibility (package.json#browserslist), API (openapi.yaml)
no marker in this repo: Performance
ℹ️ .ai/features/ does not exist yet — normal if you are setting spechelix up laterqagent version (also --version, -v) reports the hub on its own — useful
when a workspace and a clone disagree about which one is answering:
$ npx qagent version
@abiezon/qagent 0.7.0
installed package: /path/node_modules/@abiezon/qagent
node: v20.20.2
modes: openspec, spechelix, ia-cgr, manual, customIt distinguishes installed package from clone because the two fail
differently: a clone can be behind main, and a package cannot regenerate
artifacts.
Other modes: --mode openspec, --mode ia-cgr (the SDD hub that preceded
Spechelix, same .ai/features/ layout), --mode manual (no spec tool), or run
init with no --mode for the interactive menu. Full detail in
qagent --help.
Where the artifacts land
QA_DIR is not something to remember to pass. Instrumenting a repo declares it
and creates the directory, because a test plan the team cannot open and a run
report that lives in somebody's chat scrollback are artifacts that may as well
not have been produced.
The default comes from the mode — see the table above:
.ai/qa for spechelix/ia-cgr, whose repos already keep this kind of file
under .ai/, and .qa for the rest. openspec/ deliberately does not get
one: that directory belongs to OpenSpec's change flow, and qagent stays out of
directories another tool manages.
$ npx qagent init --mode manual
🧭 qagent — declaring SOURCE_MODE=standalone
📋 .qagent/config.yaml: SOURCE_MODE=standalone
📁 .qagent/config.yaml: QA_DIR=.qa (default for standalone)
📂 .qa/ created — test plans, coverage reports and run reports land hereSOURCE_MODE: standalone
QA_DIR: .qaTwo flags adjust it, and neither is needed for the normal case:
--qa-dir <path>— a different directory. A reinstall never relocates an existing one: what the repo already declared wins over the default.--no-qa-dir— opt out entirely. Nothing is written to the repo and every artifact comes back in the chat, as it did before this was the default. It cannot be combined with--knowledge, which needs somewhere to live.
Both — and --knowledge and --credentials — require --mode. Without it
the command opens the menu, which asks for each of those settings,
so a flag passed alongside would be answered over. It is refused rather than
ignored: a setting you believe landed and did not is worse than an error.
What lands there:
| Artifact | What it holds | On a rerun |
|---|---|---|
| <QA_DIR>/test-plan-<slug>.md | Test cases per acceptance criterion, across the seven test dimensions. | Asks first — reused and extended by a later run, never regenerated over |
| <QA_DIR>/coverage-audit-<slug>.md | What the automated suite does and does not cover, per criterion. | Overwritten — it describes the suite at a moment, and the previous moment is not an artifact |
| <QA_DIR>/qa-run-<slug>.md | The consolidated report of a full QA run, ending in a verdict. | Overwritten — it is a snapshot of one cycle, and the history lives in the knowledge ledgers |
bug-triage and e2e-execute write none of them — the first produces a work
item, which belongs to the spec tool's format; the second produces an execution
result, not a design artifact.
The readable copy, and not drowning in artifacts
Three artifacts per run, flat in one directory, forever. A year in, nobody can
tell a finished item from a live one — and nobody outside the team was ever
going to read a Markdown file in a diff viewer anyway. Two commands, both
working only inside QA_DIR:
npx qagent report --item HU-002 # -> <QA_DIR>/reports/HU-002.html
npx qagent archive --item HU-002 # -> <QA_DIR>/_archive/<year>/HU-002/report assembles that item's test plan, coverage audit and run report into one
self-contained page — no stylesheet, font or script from anywhere, because it
lands in your repo and may be opened from a file:// URL or committed. It
renders what the artifacts say and adds no verdict of its own; the Markdown
stays the source of truth. With no --item, it renders every item.
The page speaks your language. It is the one artifact whose wording qagent emits rather than the model, so it follows the conversation — and if you ask for a specific language, that wins over everything:
npx qagent report --item HU-002 --lang ptpt-BR resolves to pt; en, pt and es ship with label sets. Pin it per
repo with REPORT_LANG: pt in .qagent/config.yaml (a re-run of init keeps
it) and --lang still overrides. A language with no label set falls back to
English and says so — the artifacts it renders are untouched either way,
since the run wrote those in the right language to begin with.
Adding a language is a labels.yaml edit, not a code change, and
the build refuses a half-done one: a missing key, a dropped {when}, or markup
in a label that gets escaped all fail npm run render:check rather than
rendering undefined in somebody's report.
archive files a concluded item's artifacts away and refreshes the index.
The HTML report deliberately stays in reports/ — a link somebody was handed
must not break the day the item is archived. It refuses to overwrite an existing
archived artifact rather than silently replacing the record.
Both rewrite <QA_DIR>/INDEX.md, one row per item: state, artifacts, report
link, last update — in the same language as the report beside it. It does
not carry each run's verdict: that lives in the report, written in whatever
language the operator uses, and an index that pattern-matched it would be right
in English and confidently wrong in Portuguese.
post-verdict runs both for you, on different
conditions:
- The report, on every verdict — approved, reproved, blocked, or a mode with
no closing flow. Making it conditional on a closure would deliver the readable
version exactly where it is least needed: an approved run is the one nobody
has to read carefully, and a reproved one is the one somebody sits down
with — which case failed, on what evidence, which
FIXit became. - The archive, only on the closed track. A reproved or held item stays
active; it is the working record of something still open. In
custom/standalonethe archive is never automatic — nothing tells qagent the item concluded there — so the run names the command instead.
The boundary is worth stating twice: this index and this archive are qagent's
own, inside QA_DIR. Archiving specs — .ai/features/, its INDEX.md,
the _archive/<year>/ next to it — remains the spec tool's, untouched.
The table, the writes line of every command surface, and the README seeded
into the QA directory all come from the artifacts: registry in
commands.yaml — see Generation.
The path is relative to the target repo root and validated: absolute, or
escaping the repo with ../, is refused — for the per-mode defaults too, which
are checked when sources.yaml is rendered rather than at
install time. QA_DIR never changes where the acceptance criterion is read:
a spechelix repo still reads .ai/features/ and now writes its QA artifacts
next door, in .ai/qa/.
scripts/setup.sh still works as a shim —
bash ../qagent/scripts/setup.sh --mode spechelix translates to init --mode
spechelix and delegates. It exists so target repos and CI that already called
that path do not break; for new usage, prefer the CLI directly.
Checking the run against its own bar
qa-run ends with a checklist — all seven dimensions present, a verdict at the
end, the HTML rendered, the evidence where the closing flow looks. Until
recently exactly one of those was verified by anything other than the agent
remembering, which is the failure this hub is built to catch in other people's
work.
npx qagent check --item HU-002❌ HU-002 — 3 failed, 0 not verifiable
✅ report: the run report exists.
✅ dimensions: all seven dimensions appear in the report.
✅ verdict: the report ends with a verdict (approved).
❌ report-html: no HTML report — run 'qagent report --item <id> --lang <language>'.
❌ indexed: the item is missing from INDEX.md — 'qagent report' rewrites it.
❌ evidence: .ai/features/QA-HU-002-*.md is not on disk — the closing flow would run against nothing.It exits non-zero on a failure, so nothing downstream treats an incomplete cycle as finished. Two things it deliberately does not do:
- It never guesses. The report is prose in the operator's language, so the
dimensions are matched against the names declared per language — and a report
in a language with no table comes back not verifiable here, not "Security
is missing". Same for files changed outside
QA_DIR: qagent cannot tell a change this run made from work already in progress, so it surfaces them and calls them nothing. - It checks presence, never judgment. Whether a dimension was verified honestly is not readable off a document, and the checklist items that say "did not observe" stay the agent's to answer.
The machine interface
qagent's primary consumer is an agent following a playbook, not a person reading
a terminal — so the commands an agent runs during a QA cycle answer --json:
status, handoff, report, archive, version.
npx qagent status . --json
npx qagent handoff --verdict passed --item HU-002 --jsonOne JSON object on stdout and nothing else — human lines are suppressed rather
than interleaved, because a payload with prose above it is not parseable.
Failures are JSON too, with a stable error.code (no-qa-dir,
no-artifacts, archive-clash, …): getting structure on success and prose on
failure means parsing prose exactly when things went wrong.
schema is versioned and the field names are a contract. A playbook that
branches on plan.track breaks silently if a field is renamed — the run keeps
exiting 0 and simply stops closing items — so a rename bumps schema rather
than landing as a refactor.
Flags accept both --flag value and --flag=value. --json on a command that
does not speak it is an error, never a silent fallback to prose.
Running a playbook
The CLI only instruments the repo. QA itself is run by an AI agent reading
the playbooks — there is no qagent test that runs tests by itself.
By command, in the tool you already use
With the hub installed as a plugin (or cloned into the workspace), each playbook is exposed as a named command, so you do not have to describe the task in prose every time:
| Command | Tool surface |
|---|---|
| qagent-run | Claude Code skill, Codex skill, Antigravity skill, Copilot prompt, Cursor command |
| qagent-test-plan | idem |
| qagent-coverage-audit | idem |
| qagent-bug-triage | idem |
| qagent-e2e-execute | idem |
| qagent-post-verdict | idem |
/qagent-coverage-auditqagent-run is the one that chains the others — see
The full flow in one command.
The qagent- prefix is deliberate: it keeps these from colliding with the
skills of another hub installed in the same workspace.
Each command is a thin pointer at its playbook — it states what the
playbook does, when to use it, and what it outputs, then hands over. It never
restates the procedure, which is what would let six copies drift from the
playbook the first time it changes. All forty-two files are generated from
commands.yaml.
Installing the npm package is enough for the plugin-based tools: the
tarball carries .claude-plugin/, .codex-plugin/ and .antigravity-plugin/
with the trees they name, so Claude Code, Codex and Antigravity can be pointed
at node_modules/@abiezon/qagent and load the commands from there. Codex's
manifest points at a root skills/ rather than at .agents/skills/ — a
plugin's skills live at skills/, and a manifest naming any other path is
rejected at install time. .agents/skills/ stays where it was, for Codex
reading this as a repository.
Antigravity's plugin is the whole .antigravity-plugin/ directory — manifest,
its own skills/, and the three read-only agents in its own agents/ — and it
installs into the operator's home rather than being pointed at:
qagent install --antigravity-pluginThat copies the directory to ~/.gemini/config/plugins/qagent and rewrites
every pointer to this hub's absolute path, since nothing under ~/.gemini is
relative to a playbooks/ any more. It is global: no --mode, no repo
instrumented. Rerun it after moving the hub; files you edited yourself are left
alone.
Copilot and Cursor discover their files by scanning the project root, and
nothing scans inside node_modules — for those, the commands only apply in a
clone of the hub, or copied into the project. The same is true of Antigravity's
two repository surfaces (.agents/skills/, .agent/skills/), which is what
the plugin above exists to sidestep.
By prose, from anywhere
Naming the playbook always works, whatever the surface:
Read
node_modules/@abiezon/qagent/AGENTS.mdand run thecoverage-auditplaybook for HU-002.
In an already-instrumented repo, the AGENTS.md pointer block leads the agent
to the hub, so naming the playbook is usually enough.
What happens next, in order: source-resolution reads .qagent/config.yaml →
resolves the mode → reads the acceptance criterion from where that mode says →
runs the requested playbook → writes to QA_DIR if one is set, or returns in
the chat.
| Ask for | Playbook | Output |
|---|---|---|
| The whole QA cycle, in one go | qa-run | Plan + coverage + execution + items, one report |
| Test cases before implementing | test-plan | Cases per criterion |
| Whether the suite covers a feature | coverage-audit | Gap report |
| Structure a raw bug report | bug-triage | Work item in the repo's format |
| Validate by running the app | e2e-execute | Verdict per case + evidence |
The full flow in one command
Asking for one phase at a time is fine when you want exactly that phase. When what you want is QA, ask for the run:
/qagent-run HU-003qa-run goes from the request to the verdict without
handing control back between phases:
- Dimension scoping — the seven dimensions
(
test-dimensions.md) are scoped in or ruled out from evidence, once, for the whole run. - Test plan — reuses
<QA_DIR>/test-plan-<slug>.mdif it is already there (a hand-revised plan is never regenerated over, only extended with cases the criterion gained). Otherwise generates it from the source the installed mode declares —spechelix,ia-cgr,OpenSpec,custom— and saves it toQA_DIR. With neither plan nor readable criterion, it stops and says exactly that: write the plan, save it inQA_DIR, run again. - Coverage audit — plus the repo's own suite actually run (inside Docker if the repo has one), because a case covered by a currently red test is not covered.
- E2E execution — against the running application, skipping only what a green automated test already covers, and saying which. It diagnoses before it blocks: a preflight checks credential, login, target and data, and where the plan named a record this environment does not have, the run finds one that exists and says it substituted. Blocking is the last rung of a ladder (a session the operator opened → Playwright MCP → Playwright CLI → authenticated HTTP probe), never the first answer. It starts by asking whether a session already exists, so an operator with an authenticated tab is never asked to log in again, and it recognizes an identity provider (Keycloak and friends) from the redirect — where two sessions expire independently and a token that dies mid-run must not be reported as a product failure. When the run cannot log in at all — the agent's policy forbids handling passwords, or there is a second factor no software passes — that is a handoff, not a block: everything not needing the session is verified first, and the operator spends one login instead of an investigation.
- Work items — every
Failedcaused by real incorrect behavior, in the format the repo's spec tool reads, deduplicated against what is already open. - One consolidated report, written to
<QA_DIR>/qa-run-<slug>.md, with the seven-row dimension matrix and ending in a verdict. A setup block, a real failure and a case unblocked since the last round stay distinguishable at a glance — the last one carries its earlier evidence markedsuperseded, rather than quietly replacing it. - The handoff back to the spec tool — the verdict leaves QA_DIR and reaches whatever actually closes the item, plus the HTML report for whoever asked. See after the verdict below.
Removing the stops between phases does not remove the judgment calls. The run
still asks — once, batched — when the source is ambiguous, when QA_DIR holds
several candidate plans, when more than one compose file could be the test
environment, and before bringing the application up or acting on an
environment that is not disposable. A conflict between a test and the criterion
never stops the run and never opens an item on its own: it lands in the
report's awaiting your decision section with both pieces of evidence.
A phase that fails does not abort the run — no suite, no reachable instance, a case that cannot be executed: each degrades into a reported finding and the next phase still runs. The only early exit is having no acceptance criterion at all.
Two things worth knowing before you ask:
- The audit playbooks are read-only. They report gaps and write no tests, even when the fix looks obvious. Writing the tests is implementation work.
- Only one kind of finding becomes a work item. A coverage gap is a risk,
not a defect, and never opens a
FIX. The exception is a test asserting the opposite of the criterion — and even then the agent reports both pieces of evidence and asks which side is authoritative.
After the verdict
A verdict is not a closure. The run writes qa-run-HU-002.md into QA_DIR,
concludes the HU passed, and stops — while the spec tool is looking for its
own evidence file, under its own name, in its own directory. So the QA report
sits there, perfectly finished, next to an HU-002 that is still
em-andamento, and nobody notices because nothing failed.
post-verdict.md is the step that closes that
gap. It runs after the report, per mode:
| Mode | QA evidence it leaves | Approved verdict dispatches | Failed verdict dispatches |
|---|---|---|---|
| spechelix | .ai/features/QA-<id>-<slug>.md | sdd-execute <id> | sdd-execute <fix> |
| ia-cgr | .ai/features/QA-<id>-<slug>.md | sdd-execute <id> | sdd-execute <fix> |
| OpenSpec | — | openspec archive <change> | openspec validate <change> |
| custom | — | not automatic — the repo's convention is its own, and qagent does not know which command closes an item here | — |
| standalone | — | not automatic — there is no spec tool in this repo to close anything, so the verdict itself is the deliverable | — |
Five rules hold across every row:
A dispatch is invoked, not printed. In the three modes that have a flow —
spechelix,ia-cgr,OpenSpec— the agent running the cycle makes the call itself, and that includes the reproved verdict: theFIX(or the change) goes straight tosdd-execute/ the repo's own OpenSpec flow, without coming back to ask. A run that ends with a defect recorded and its fix flow never invoked stopped one step short of its purpose. qagent itself spawns nothing — it computes the filled command; the agent runs it. It falls back to handing the command over only when the tool is not reachable, and asks only for a name it must not invent.A declared mode is not proof the tool is installed.
sdd-executeships with spechelix/ia-cgr, not with the target repo — a repo carrying.ai/features/, opened by someone who never installed the hub, has the source and not the command. So the handoff looks for it (in the repo, and beside it in a workspace) before claiming a dispatch. Not finding it means reports only: the evidence and theFIXare still written, because those are files; the dispatch is not, because it is a command. The reports go to whoever develops the repo. It is never reported as "this cannot be closed" — the check is local, so if you do havesdd-execute, the plan prints the command filled in and ready.OpenSpecgets no such check:openspec/is the mode's marker and the change flow's directory, so having it is the configuration already saying this is the tool.qagent generates the evidence and dispatches the flow — it does not close the item. Setting an
HUtoconcluído, updatingINDEX.md, moving a spec into_archive/: all of that belongs tosdd-execute/spec-index, and a QA hub reaching into it is a QA hub that corrupts spec state on its first edge case. The full report stays inQA_DIR; only the evidence the closing flow detects is written where the spec tool reads.An unresolved name is asked, never invented. Which HU a
FEAT-002belongs to is read from itshu:front-matter, not assumed from the matching number; an OpenSpec change is named by the operator when more than one is active. The command gets printed instead of dispatched.A mode with no closing flow says so, and the reports are the deliverable. Running manually,
customandstandalonereport the verdict, the artifact, why nothing is automatic, and what to take to the developer — the one thing that never happens is silence.
A blocked verdict, a conflict between the suite and the criterion, or a repo whose declared mode does not match what is on disk all stop at the same place: nothing is dispatched, and the operator gets the reason plus the next command.
Then the step renders the operator's copy and, on the closed track, archives the run's artifacts — see the readable copy.
The plan is deterministic, and you can ask for it without running QA at all — useful for exactly the case above, a run that already passed and never closed anything:
npx qagent handoff --verdict passed --item HU-002 --slug loginIt prints the plan and dispatches nothing: running sdd-execute is the agent's
or the operator's, which is the same boundary that keeps qagent out of the spec
tool's state. qagent status shows the same per-mode summary before any run
produces a verdict.
Updating the hub
Run it where the package.json declaring qagent lives — the workspace root,
not inside each target repo (those only carry the config, not the dependency):
npm update @abiezon/qagentThe installed version stays recorded in the consumer's package.json and
locked by package-lock.json, which makes the environment reproducible. To go
back, npm i @abiezon/[email protected].
In a workspace with several target repos, installing at the root (npm i -D
@abiezon/qagent) makes npx qagent available to all of them from a single
declared version.
npm update stops working at a minor bump
While the hub is on 0.x, npm's ^ pins the second number: ^0.6.0
allows >=0.6.0 <0.7.0. That is deliberate — before 1.0.0, npm treats
minor as potentially breaking.
In practice: npm update brings 0.6.1, 0.6.9… and stops silently when
0.7.0 ships. No error, no warning — just a version that never arrives. To
cross over, you have to be explicit:
npm i -D @abiezon/qagent@latestThat rewrites the declared range and npm update works within it again. To
find out whether you are in this situation:
npm outdated @abiezon/qagentWanted is the most your range allows; Latest is what exists. When the two
differ, npm update will not resolve it.
Using npx without installing, none of this applies — but use
npx @abiezon/qagent@latest: without @latest, npx may serve a local cache
instead of resolving the newest version.
Configuration
All qagent configuration lives in one file per target repo,
<repo>/.qagent/config.yaml. There is no global or per-user configuration —
what applies to a repo is declared inside it.
SOURCE_MODE: spechelix # required — where the acceptance criterion lives
QA_DIR: .ai/qa # where QA artifacts are persisted — set for you, per mode
KNOWLEDGE: true # optional — learn about the project across runs| Key | Values | Effect |
|---|---|---|
| SOURCE_MODE | spechelix, ia-cgr, OpenSpec, custom, standalone | Decides what step 0 reads. Without it, source-resolution runs automatic detection |
| QA_DIR | path relative to the repo root | test-plan, coverage-audit and qa-run write there. Declared and created at install time from the mode's default; --qa-dir overrides it, --no-qa-dir opts out |
| KNOWLEDGE | true (or absent) | Turns on the learning loop in <QA_DIR>/knowledge/. Needs QA_DIR. Off by default |
| CREDENTIALS | true (or absent) | Creates .qagent/credentials/, git-ignored, for the test logins a run needs. Off by default |
All three are written by the CLI, not by hand — the first two on any install:
npx qagent install --mode spechelix --knowledgeAnd checked with npx qagent status, which also flags inconsistencies — a
declared mode whose marker does not exist, or another mode's marker present in
the repo.
Editing the YAML by hand works (it is read at runtime), but the CLI validates
what it writes: it refuses an unknown mode, and refuses a QA_DIR that is
absolute or escapes the repo with ../.
What is not configurable
By decision, not by limitation:
- Where playbooks look for the test suite —
coverage-auditdiscovers it by convention and asks when ambiguous. An override would become configuration that goes stale without anyone noticing. - Severity, minimum coverage, what counts as testable — they live in
guardrails/qa-quality.mdand apply equally to every repo. They are a yardstick, not a preference. - Running through Docker — if the repo has a container, that is where it
runs. The run checks for it before its first command (
qagent statusprints the🐳line) and installs nothing on the host to get around it. See the guardrail. - The language of the instructions — they are English on purpose. What is adjustable is the conversation, and that follows the operator without any configuration.
Migrating from the old format
A repo configured by an older version has .qagent/config in KEY=VALUE
format. It is still read normally — status warns about the pending
migration and the next init converts it to .qagent/config.yaml, carrying
over any other key it held, and removes the old file. Comments from the old
format do not survive: YAML carries values only.
Playbooks
| Playbook | What it does |
|---|---|
| source-resolution.md | Common step 0 — resolves the acceptance-criterion source |
| qa-run.md | The full flow end to end — orchestrates the five below and delivers one report with a verdict |
| bug-triage.md | Turns a raw bug report into a work item |
| test-plan.md | Generates test cases (positive/negative/edge case) from the acceptance criterion |
| coverage-audit.md | Audits whether the automated suite covers the acceptance criterion |
| e2e-execute.md | Validates the acceptance criterion by running the real application |
| post-verdict.md | Hands the verdict back to the spec tool — QA evidence where its closing flow looks, and the dispatch that closes the item |
Quality guardrails (severity, what counts as "testable", minimum coverage,
running through Docker whenever the target repo has it, and the language
rule) in guardrails/qa-quality.md.
Knowledge: the run that learns
Off by default. Turned on per repo, and it lives in the QA_DIR the install
already created:
npx qagent init --mode spechelix --knowledgeWithout it, run twenty costs what run one cost: rediscovering which compose
service holds the app, which user can log in, where the listing endpoint is,
and which three tests were already red before anybody touched anything. With
it, each pass through the QA cycle is one turn of a loop —
playbooks/knowledge.md:
- Read
<QA_DIR>/knowledge/before scoping anything. - Use it — skip the rediscovery, compare against recorded baselines, weight the dimension scoping by recorded risk.
- Verify — an entry that held is confirmed by that use; one that failed is corrected or deleted in the same run.
- Write back what a future run would otherwise pay to learn again.
A fixed set of files, so no run invents its own taxonomy — and two shapes, because knowledge has two and one storage does not fit both:
| Shape | Files | Rule |
|---|---|---|
| Prose, read whole | environment.md (how the app comes up, the suite command that worked, the suite's standing state), access.md (fixture users — where credentials come from, never their values), surface.md (routes, selectors, which endpoints are AJAX), risk.md (fragile modules), fixtures.md (records this environment has — and the ones known to be empty), decisions.md (what the operator already settled) | Current state — rewritten in place |
| Ledger, one line per observation | measurements.ndjson (the series a later run compares its numbers against), suite-state.ndjson (tests red independently of any change), findings.ndjson (findings per module and dimension) | Append-only — a wrong line is superseded, never edited |
The split exists because "rewrite in place" is right for prose and wrong for
measurements: overwriting 1.8s with 4.2s destroys the very evidence that makes
it a finding. The ledgers are NDJSON — git diffs and
merges them line by line, reading one costs no dependency, and a new field never
invalidates an old line. If a history ever outgrows plain reading, an index.db
built from the ledgers is derived and gitignored; the ledgers stay the truth.
What it changes, concretely: performance stops being a lonely number — a baseline of 1.8s makes 4.2s next month a finding even where no SLA exists; scoping gains evidence, so a module with two past authorization findings pulls security into scope on record; and the run spends its one interruption on something new instead of re-asking which compose file is the test one.
Three constraints keep it from becoming a liability:
- It is a cache, never an authority. Knowledge says how to test this repo, not how the repo should behave. Disagreement with the acceptance criterion is resolved for the source, every time, and the stale entry is fixed on the spot.
- No secrets, ever — no token, password or personal datum, not even as evidence. Where a credential comes from, never what it is.
- No standing authorizations. "Yes, run the security pass against staging" authorized that run. The gates are asked again on the next one.
Every entry carries observed (date + run), evidence, and invalidated-by —
what would make it false — so the next run can re-check cheaply instead of
trusting blindly. npx qagent status shows whether the loop is on and how many
files it has.
Test credentials
Off by default. Answered in the menu, or --credentials:
npx qagent install --mode spechelix --credentialsIt creates .qagent/credentials/ with a README, an example file, and a
.gitignore that ignores everything else in the directory. Copy
credentials.example.yaml to credentials.yaml, fill it in, and the values are
invisible to git — a run reads them from there instead of asking you for a
password in the chat every time.
It sits under .qagent/ and not under QA_DIR deliberately: QA_DIR holds
team documents meant to be committed, and mixing "commit this" with "never
commit this" in one tree is how a secret reaches the history. --no-credentials
only drops the declaration — nothing in the directory is ever deleted by a flag.
credentials.example.yaml is the schema the playbooks read, not a loose
suggestion: a run looks up users.<name>.role to pick the login a case needs
and api.base_url to build a request. Keep the key names; extra keys of your
own are ignored, renamed ones are simply not found and the run falls back to
asking you in the chat.
That example is generated from credentials.yaml, the same
declaration the CLI parses — the two drifting used to fail silently, with the
run reporting "no credential" and blocking every authenticated case. Each field
there also declares whether it may ever be said out loud, and the sanitized view
below is built from that flag: a field nobody marked reportable cannot reach
status, a report or a work item, and the generator refuses to mark a password,
token, secret or base_url as reportable at all.
qagent status reports what is there without reading a secret out loud —
the declared environment, which users exist and their roles, whether an API is
configured — and shouts if the environment looks like production:
🔐 CREDENTIALS=true (.qagent/credentials/)
environment: local · users: admin (admin), vendedor (seller) · api configuredThat sanitized shape is also what a QA run may put in a report: credential found, role, environment, login ok or rejected. Never a value.
The file on disk is what a run keys off, not the flag. CREDENTIALS: true
is your declaration; a repo can perfectly well have
.qagent/credentials/credentials.yaml without it — created by hand, or written
by an older version of the CLI. So the playbooks check for the file, and
status says so when the two disagree:
🔐 .qagent/credentials/credentials.yaml exists but CREDENTIALS is not declared
runs read the file anyway — 'qagent init --mode spechelix --credentials' declares itThis matters because of one specific failure: a run reaches a redirect to the
login page, concludes "authentication required", and blocks every authenticated
case without ever opening the credentials file — with a valid fixture user
sitting in it. So a redirect to login is now an explicit ordered step in
e2e-execute: open the file, pick the entry by
role, attempt the login, then classify. A run may not report no credential,
credential rejected, or a handoff for a login it never tried — the one
exception being an agent whose own policy forbids handling passwords, which is
knowable before any attempt and is a handoff from the start. The preflight line
names the key it used (users.admin), never the value, which is what makes a
skipped credential visible in the report instead of invisible.
The seeded README carries the rules, and they are the point of the feature:
- Test and development environments only. Never production credentials, not even temporarily. A run that would need production does not get production.
- Least privilege — the smallest role that exercises the flow, not your own account and not an admin token because it was faster.
- Never share the file — not by chat, email, ticket or screenshot. A teammate seeds their own, or it goes through a password manager. What travels through a chat lives in somebody's backup forever.
- Rotate what leaks. Deleting a committed file does not undo it; the only fix that works is invalidating the credential.
- Nothing from here reaches a report. Playbooks use these values and never transcribe them — evidence gets truncated or described.
LGPD
Filling a test environment with a copy of production data is processing of personal data under Lei 13.709/2018, needing a legal basis and a purpose — "testing" is neither by itself. The practical guidance the seeded README spells out: prefer synthetic data (anonymized data leaves the law's scope, art. 12, and synthetic data was never personal); pseudonymizing is not anonymizing, since the remaining attributes usually still re-identify; minimize (art. 6º, III) — a test needs the records the case exercises, not a dump; a test environment is typically the weakest one, and art. 46 does not lower its bar for that; and a leak of real personal data is an incident (art. 48) whether or not it happened in production. Operational guidance for QA, not legal advice.
Test dimensions
The acceptance criterion says what must work; it rarely says in how many ways
it can fail. guardrails/test-dimensions.md
defines the seven ways — its registry generated from
dimensions.yaml, its method hand-written — and every
playbook applies them — the plan designs
cases per dimension, the audit classifies coverage per dimension, the execution
verifies them against the running app.
| Dimension | Applies when | Scoped by |
|---|---|---|
| Functional | Always — it is the floor | Always |
| Regression | Always, on a product already in use | Always |
| Performance | The criterion states a number, or the change touches something whose cost grows with data | Marker: lighthouserc.json, lighthouserc.js, .lighthouserc.yml, performance-budget.json |
| Security | Baseline always; deeper on auth, permissions, personal data, upload, external input | Always |
| Usability | The change has a user interface | The run decides from the change |
| Compatibility | The repo declares a support matrix, or a consumer contract changed | Marker: package.json#browserslist, package.json#engines, .browserslistrc, .nvmrc |
| API | The product exposes an API the criterion touches | Marker: openapi.yaml, openapi.json, swagger.json, api/openapi.yaml, docs/openapi.yaml |
The Scoped by column is what qagent status can answer for you: where a
dimension has a marker, the CLI looks for it and reports what the repo actually
carries, instead of leaving the run to re-derive it every time. A marker is a
floor, not a ceiling — performance is in scope for a change touching a query in
a loop whether or not a budget file exists.
Three rules keep this from becoming theater:
- Nothing is silently absent. A dimension is in scope with a verdict, or out of scope with a one-line reason. A missing row and a passing row must never look the same.
- Claim only what you observed. No load tool, browser farm or scanner gets installed in your repo, so some checks end as not verifiable here, naming what they would take. That is a real result — "no performance issues found" after measuring nothing is not.
- Security stays inside the fence. Only the target application, only in an environment you authorized, no exploit development, no destructive payloads, and evidence redacted before it reaches a tracker.
Scoping is decided from evidence and declared in the report, not asked as a
questionnaire at the start of the run — and you can override it in the request
(/qagent-run HU-003 — só segurança e API).
Read-only agents
Three agents run the playbooks above independently, always read-only:
coverage-auditor,
test-plan-reviewer,
bug-reproducibility-checker.
Each has a single source of truth in agents/*.yaml —
description, tools, what to read, and the read-only constraint. The four
native adapters (Claude, .agents Protocol, Antigravity as a repository, and
Antigravity as a plugin) are generated from there — see
Generation.
Only the two an explicit plugin path reaches ship in the package —
.claude/agents/ and .antigravity-plugin/agents/. The root-scanned ones stay
in a clone of the repository: nothing looks inside node_modules for them.
Supported tools
A thin adapter in each native format — none duplicates content, all point back
at the playbooks: Claude Code (.claude/), Gemini CLI (GEMINI.md), GitHub
Copilot (.github/), Cursor (.cursor/commands/), and the
.agents Protocol (.agents/), which is
where Codex CLI, Google Antigravity and Cursor all read from. Codex also reads
AGENTS.md at the root natively, and — installed as a plugin rather than opened
as a repository — the canonical skills/ tree its manifest requires. .agent/
(singular) is kept for older Antigravity, which has since moved its default to
.agents/skills/.
Antigravity is also a first-class plugin:
.antigravity-plugin/ carries a plugin.json
generated from package.json (so version, description, license and keywords
cannot drift from what is published) beside the two trees it declares — its own
skills/ and its own agents/. qagent install --antigravity-plugin puts it
at ~/.gemini/config/plugins/qagent.
These are read from the repo root or from a user-global directory — no tool
documents scanning node_modules, so installing the npm package gives you the
CLI and the playbooks, not the invocable commands.
npx qagent install --mode <mode> --commandsThat copies them in, at .agents/skills/, .claude/skills/, .claude/agents/
and .github/prompts/, rewriting each pointer so it resolves from where the copy
lands — a verbatim copy would have every command point at a playbooks/
directory your repo does not have. Rerun to refresh; files you wrote yourself are
left alone. The full table is in AGENTS.md.
Generation
Anything that would otherwise be written out in more than one place is declared once in YAML and generated. The rule for what belongs there: the closed list goes, the judgment stays. A set of modes, artifacts, dimensions or severity levels is data; when to apply one is prose, and turning that into config is how a guardrail starts reading like a setting somebody may switch off.
The urgent case is a copy living in code — a template string in the CLI is
the one npm run render:check can never keep honest, and it is where the QA
directory's README, the knowledge file list and the credentials example each
drifted before being moved here.
| Source | Generates |
|---|---|
| agents/*.yaml | The read-only agent adapters: .claude/agents/, .agents/agents/, .agent/skills/, .antigravity-plugin/agents/ |
| sources.yaml | scripts/sources.generated.mjs (imported by the CLI), the modes table and the post-verdict table in this README |
| dimensions.yaml | scripts/dimensions.generated.mjs (marker detection in qagent status), the dimensions table in this README and in the guardrail |
| commands.yaml | The invocation surfaces in seven formats, the .claude-plugin//.codex-plugin//.antigravity-plugin/ manifests, and — from its artifacts: registry — scripts/artifacts.generated.mjs (imported by the CLI to seed the QA directory) and the artifacts table in this README |
| knowledge.yaml | scripts/knowledge.generated.mjs (the CLI seeds <QA_DIR>/knowledge/ and counts its entries from it), the file and ledger-field tables in knowledge.md, and the shapes table in this README |
| credentials.yaml | scripts/credentials.generated.mjs — the seeded credentials.example.yaml, the schema table in e2e-execute.md, and the reportable gate that decides what qagent status may print |
| labels.yaml | scripts/labels.generated.mjs — the HTML report's wording per language, and the order its sections appear in |
| severity.yaml | The taxonomy table in qa-quality.md and the severity hint in bug-report-TEMPLATE.md. No .mjs: nothing in the CLI classifies a finding |
npm install && npm run rendernpm run render:check (or qagent render --check) writes nothing and exits
with status 1 if any generated file diverges from the YAML. It is what runs in
.github/workflows/render-check.yml on
every push to main and every PR, catching all three cases: YAML edited
without running render, a generated file edited by hand, and a generated file
nobody committed.
The same workflow runs npm test — Node's own test runner, no dependency, over
the CLI's decision logic. Most of this hub is prose a model follows and cannot
be unit-tested; what can be is the deterministic part, and today that is the
post-verdict handoff, whose failure mode is silence rather
than a stack trace.
npm testEditing an agent or a source mode means editing the YAML, never the generated
file. The generated files are committed on purpose: the hub has to work for
someone who clones the repo and reads AGENTS.md, without running a build.
Generation is a build step, never an install step — and it is where schema
validation is concentrated (duplicate alias, gap in menu.order), so the CLI
receives already-checked data.
Note the split of responsibility: sources.yaml is the hub's config and
can therefore be pre-processed; .qagent/config.yaml is the target repo's
config, hand-written, and therefore parsed at runtime — which is what makes
js-yaml a real dependency rather than a build one.
What stays out by decision: the playbooks, the guardrails and
source-resolution.md remain hand-written markdown. The rule of thumb is that
YAML serves what a program needs to branch on, and markdown what a model needs
to read.
For agents
AGENTS.md is the entry point — read that file first if you are
the agent running a task in this hub or from it.
