npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@abiezon/qagent

v0.12.1

Published

Agentic QA hub — spec-tool agnostic

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 spechelix

Or installed, for recurring use:

npm i -D @abiezon/qagent

qagent 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
    > a

With 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] y

Every 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 later

That 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 later

qagent 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, custom

It 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 here
SOURCE_MODE: standalone
QA_DIR: .qa

Two 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 --credentialsrequire --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 pt

pt-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 FIX it 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/standalone the 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 --json

One 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-audit

qagent-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-plugin

That 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.md and run the coverage-audit playbook 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-003

qa-run goes from the request to the verdict without handing control back between phases:

  1. Dimension scoping — the seven dimensions (test-dimensions.md) are scoped in or ruled out from evidence, once, for the whole run.
  2. Test plan — reuses <QA_DIR>/test-plan-<slug>.md if 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 to QA_DIR. With neither plan nor readable criterion, it stops and says exactly that: write the plan, save it in QA_DIR, run again.
  3. 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.
  4. 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.
  5. Work items — every Failed caused by real incorrect behavior, in the format the repo's spec tool reads, deduplicated against what is already open.
  6. 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 marked superseded, rather than quietly replacing it.
  7. 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: the FIX (or the change) goes straight to sdd-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-execute ships 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 the FIX are 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 have sdd-execute, the plan prints the command filled in and ready. OpenSpec gets 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 HU to concluído, updating INDEX.md, moving a spec into _archive/: all of that belongs to sdd-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 in QA_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-002 belongs to is read from its hu: 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, custom and standalone report 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 login

It 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/qagent

The 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@latest

That rewrites the declared range and npm update works within it again. To find out whether you are in this situation:

npm outdated @abiezon/qagent

Wanted 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 --knowledge

And 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 suitecoverage-audit discovers 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.md and 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 status prints 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 --knowledge

Without 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:

  1. Read <QA_DIR>/knowledge/ before scoping anything.
  2. Use it — skip the rediscovery, compare against recorded baselines, weight the dimension scoping by recorded risk.
  3. Verify — an entry that held is confirmed by that use; one that failed is corrected or deleted in the same run.
  4. 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 --credentials

It 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 configured

That 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 it

This 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/*.yamldescription, 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> --commands

That 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 render

npm 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 test

Editing 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.