qaas-python 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. qaas_python-0.0.1/.gitignore +24 -0
  2. qaas_python-0.0.1/ARCHITECTURE.md +476 -0
  3. qaas_python-0.0.1/BUILD_PLAN.md +306 -0
  4. qaas_python-0.0.1/CHANGELOG.md +38 -0
  5. qaas_python-0.0.1/CLAUDE.md +351 -0
  6. qaas_python-0.0.1/LICENSE +21 -0
  7. qaas_python-0.0.1/PKG-INFO +429 -0
  8. qaas_python-0.0.1/README.md +399 -0
  9. qaas_python-0.0.1/config/targets/corvid.yaml +56 -0
  10. qaas_python-0.0.1/pyproject.toml +91 -0
  11. qaas_python-0.0.1/src/qaas/adapters/__init__.py +19 -0
  12. qaas_python-0.0.1/src/qaas/adapters/tracker.py +1783 -0
  13. qaas_python-0.0.1/src/qaas/adapters/vcs.py +555 -0
  14. qaas_python-0.0.1/src/qaas/cli.py +1757 -0
  15. qaas_python-0.0.1/src/qaas/config.py +409 -0
  16. qaas_python-0.0.1/src/qaas/defaults/config/agents/api.yaml +18 -0
  17. qaas_python-0.0.1/src/qaas/defaults/config/agents/architect.yaml +21 -0
  18. qaas_python-0.0.1/src/qaas/defaults/config/agents/auditor.yaml +19 -0
  19. qaas_python-0.0.1/src/qaas/defaults/config/agents/browser.yaml +15 -0
  20. qaas_python-0.0.1/src/qaas/defaults/config/agents/dba.yaml +20 -0
  21. qaas_python-0.0.1/src/qaas/defaults/config/agents/fixer.yaml +55 -0
  22. qaas_python-0.0.1/src/qaas/defaults/config/agents/guide.yaml +23 -0
  23. qaas_python-0.0.1/src/qaas/defaults/config/agents/load.yaml +26 -0
  24. qaas_python-0.0.1/src/qaas/defaults/config/agents/mapper.yaml +19 -0
  25. qaas_python-0.0.1/src/qaas/defaults/config/agents/reporter.yaml +19 -0
  26. qaas_python-0.0.1/src/qaas/defaults/config/agents/reproducer.yaml +21 -0
  27. qaas_python-0.0.1/src/qaas/defaults/config/agents/reviewer.yaml +18 -0
  28. qaas_python-0.0.1/src/qaas/defaults/config/agents/socket.yaml +23 -0
  29. qaas_python-0.0.1/src/qaas/defaults/config/agents/triage.yaml +20 -0
  30. qaas_python-0.0.1/src/qaas/defaults/config/agents/verifier.yaml +20 -0
  31. qaas_python-0.0.1/src/qaas/defaults/config/system.yaml +64 -0
  32. qaas_python-0.0.1/src/qaas/discover.py +242 -0
  33. qaas_python-0.0.1/src/qaas/envelope.py +318 -0
  34. qaas_python-0.0.1/src/qaas/envfile.py +100 -0
  35. qaas_python-0.0.1/src/qaas/guardrails.py +589 -0
  36. qaas_python-0.0.1/src/qaas/mcp/__init__.py +0 -0
  37. qaas_python-0.0.1/src/qaas/mcp/context.py +78 -0
  38. qaas_python-0.0.1/src/qaas/mcp/contract_diff.py +1011 -0
  39. qaas_python-0.0.1/src/qaas/mcp/defect_memory.py +495 -0
  40. qaas_python-0.0.1/src/qaas/mcp/env_control.py +925 -0
  41. qaas_python-0.0.1/src/qaas/mcp/envelope_server.py +463 -0
  42. qaas_python-0.0.1/src/qaas/mcp/test_runner.py +842 -0
  43. qaas_python-0.0.1/src/qaas/mcp/tracker.py +420 -0
  44. qaas_python-0.0.1/src/qaas/mcp/vcs.py +501 -0
  45. qaas_python-0.0.1/src/qaas/paths.py +317 -0
  46. qaas_python-0.0.1/src/qaas/plugin/.claude-plugin/plugin.json +9 -0
  47. qaas_python-0.0.1/src/qaas/plugin/skills/a11y-audit/SKILL.md +34 -0
  48. qaas_python-0.0.1/src/qaas/plugin/skills/adversarial-review/SKILL.md +120 -0
  49. qaas_python-0.0.1/src/qaas/plugin/skills/api-surface-extraction/SKILL.md +38 -0
  50. qaas_python-0.0.1/src/qaas/plugin/skills/authz-matrix-check/SKILL.md +46 -0
  51. qaas_python-0.0.1/src/qaas/plugin/skills/console-error-triage/SKILL.md +39 -0
  52. qaas_python-0.0.1/src/qaas/plugin/skills/contract-test-generation/SKILL.md +36 -0
  53. qaas_python-0.0.1/src/qaas/plugin/skills/dedupe-strategy/SKILL.md +39 -0
  54. qaas_python-0.0.1/src/qaas/plugin/skills/environment-pinning/SKILL.md +35 -0
  55. qaas_python-0.0.1/src/qaas/plugin/skills/error-taxonomy/SKILL.md +42 -0
  56. qaas_python-0.0.1/src/qaas/plugin/skills/exploratory-ui-walk/SKILL.md +46 -0
  57. qaas_python-0.0.1/src/qaas/plugin/skills/failing-test-authoring/SKILL.md +47 -0
  58. qaas_python-0.0.1/src/qaas/plugin/skills/flake-detection/SKILL.md +39 -0
  59. qaas_python-0.0.1/src/qaas/plugin/skills/form-state-probe/SKILL.md +36 -0
  60. qaas_python-0.0.1/src/qaas/plugin/skills/minimal-diff-discipline/SKILL.md +70 -0
  61. qaas_python-0.0.1/src/qaas/plugin/skills/openapi-diff/SKILL.md +45 -0
  62. qaas_python-0.0.1/src/qaas/plugin/skills/ownership-resolution/SKILL.md +31 -0
  63. qaas_python-0.0.1/src/qaas/plugin/skills/product-task-graph/SKILL.md +35 -0
  64. qaas_python-0.0.1/src/qaas/plugin/skills/regression-risk-scoring/SKILL.md +59 -0
  65. qaas_python-0.0.1/src/qaas/plugin/skills/regression-suite-selection/SKILL.md +36 -0
  66. qaas_python-0.0.1/src/qaas/plugin/skills/repo-cartography/SKILL.md +38 -0
  67. qaas_python-0.0.1/src/qaas/plugin/skills/repro-minimisation/SKILL.md +41 -0
  68. qaas_python-0.0.1/src/qaas/plugin/skills/rollback-plan-authoring/SKILL.md +81 -0
  69. qaas_python-0.0.1/src/qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +67 -0
  70. qaas_python-0.0.1/src/qaas/plugin/skills/routing-rules/SKILL.md +34 -0
  71. qaas_python-0.0.1/src/qaas/plugin/skills/severity-rubric/SKILL.md +42 -0
  72. qaas_python-0.0.1/src/qaas/plugin/skills/test-first-fix/SKILL.md +66 -0
  73. qaas_python-0.0.1/src/qaas/plugin/skills/test-quality-audit/SKILL.md +58 -0
  74. qaas_python-0.0.1/src/qaas/plugin/skills/ticket-writer/SKILL.md +40 -0
  75. qaas_python-0.0.1/src/qaas/plugin/skills/verdict-reporting/SKILL.md +35 -0
  76. qaas_python-0.0.1/src/qaas/plugin/skills/verification-protocol/SKILL.md +39 -0
  77. qaas_python-0.0.1/src/qaas/prompts/API.md +44 -0
  78. qaas_python-0.0.1/src/qaas/prompts/ARCHITECT.md +80 -0
  79. qaas_python-0.0.1/src/qaas/prompts/AUDITOR.md +62 -0
  80. qaas_python-0.0.1/src/qaas/prompts/BROWSER.md +46 -0
  81. qaas_python-0.0.1/src/qaas/prompts/DBA.md +59 -0
  82. qaas_python-0.0.1/src/qaas/prompts/FIXER.md +55 -0
  83. qaas_python-0.0.1/src/qaas/prompts/GUIDE.md +94 -0
  84. qaas_python-0.0.1/src/qaas/prompts/LOAD.md +109 -0
  85. qaas_python-0.0.1/src/qaas/prompts/MAPPER.md +46 -0
  86. qaas_python-0.0.1/src/qaas/prompts/REPORTER.md +61 -0
  87. qaas_python-0.0.1/src/qaas/prompts/REPRODUCER.md +43 -0
  88. qaas_python-0.0.1/src/qaas/prompts/REVIEWER.md +53 -0
  89. qaas_python-0.0.1/src/qaas/prompts/SOCKET.md +100 -0
  90. qaas_python-0.0.1/src/qaas/prompts/TRIAGE.md +45 -0
  91. qaas_python-0.0.1/src/qaas/prompts/VERIFIER.md +41 -0
  92. qaas_python-0.0.1/src/qaas/prompts/_shared.md +45 -0
  93. qaas_python-0.0.1/src/qaas/registry.py +496 -0
  94. qaas_python-0.0.1/src/qaas/router.py +581 -0
  95. qaas_python-0.0.1/src/qaas/runner.py +210 -0
  96. qaas_python-0.0.1/src/qaas/scorecard.py +448 -0
  97. qaas_python-0.0.1/src/qaas/sdk_compat.py +52 -0
  98. qaas_python-0.0.1/src/qaas/store.py +323 -0
  99. qaas_python-0.0.1/src/qaas/target.py +287 -0
  100. qaas_python-0.0.1/src/qaas/tasks.py +438 -0
  101. qaas_python-0.0.1/src/qaas/trace.py +342 -0
  102. qaas_python-0.0.1/target-app/CODEOWNERS +15 -0
  103. qaas_python-0.0.1/target-app/api/Dockerfile +17 -0
  104. qaas_python-0.0.1/target-app/api/app/__init__.py +3 -0
  105. qaas_python-0.0.1/target-app/api/app/auth.py +113 -0
  106. qaas_python-0.0.1/target-app/api/app/config.py +38 -0
  107. qaas_python-0.0.1/target-app/api/app/db.py +36 -0
  108. qaas_python-0.0.1/target-app/api/app/errors.py +56 -0
  109. qaas_python-0.0.1/target-app/api/app/main.py +44 -0
  110. qaas_python-0.0.1/target-app/api/app/models.py +103 -0
  111. qaas_python-0.0.1/target-app/api/app/routes/__init__.py +5 -0
  112. qaas_python-0.0.1/target-app/api/app/routes/auth.py +31 -0
  113. qaas_python-0.0.1/target-app/api/app/routes/invoices.py +37 -0
  114. qaas_python-0.0.1/target-app/api/app/routes/orders.py +130 -0
  115. qaas_python-0.0.1/target-app/api/app/routes/stream.py +72 -0
  116. qaas_python-0.0.1/target-app/api/app/schemas.py +89 -0
  117. qaas_python-0.0.1/target-app/api/migrations/001_init.sql +61 -0
  118. qaas_python-0.0.1/target-app/api/pyproject.toml +22 -0
  119. qaas_python-0.0.1/target-app/api/seed/fixtures.sql +155 -0
  120. qaas_python-0.0.1/target-app/defects.yaml +361 -0
  121. qaas_python-0.0.1/target-app/docker-compose.yml +48 -0
  122. qaas_python-0.0.1/target-app/openapi.yaml +246 -0
  123. qaas_python-0.0.1/target-app/web/.gitignore +3 -0
  124. qaas_python-0.0.1/target-app/web/Dockerfile +14 -0
  125. qaas_python-0.0.1/target-app/web/index.html +12 -0
  126. qaas_python-0.0.1/target-app/web/package-lock.json +1792 -0
  127. qaas_python-0.0.1/target-app/web/package.json +24 -0
  128. qaas_python-0.0.1/target-app/web/src/api.ts +232 -0
  129. qaas_python-0.0.1/target-app/web/src/components/Button.tsx +24 -0
  130. qaas_python-0.0.1/target-app/web/src/components/Layout.tsx +53 -0
  131. qaas_python-0.0.1/target-app/web/src/components/SearchInput.tsx +24 -0
  132. qaas_python-0.0.1/target-app/web/src/main.tsx +74 -0
  133. qaas_python-0.0.1/target-app/web/src/routes/CheckoutReview.tsx +152 -0
  134. qaas_python-0.0.1/target-app/web/src/routes/Login.tsx +100 -0
  135. qaas_python-0.0.1/target-app/web/src/routes/NewOrder.tsx +258 -0
  136. qaas_python-0.0.1/target-app/web/src/routes/OrderDetail.tsx +179 -0
  137. qaas_python-0.0.1/target-app/web/src/routes/OrdersList.tsx +126 -0
  138. qaas_python-0.0.1/target-app/web/src/styles.css +430 -0
  139. qaas_python-0.0.1/target-app/web/tsconfig.json +21 -0
  140. qaas_python-0.0.1/target-app/web/vite.config.ts +28 -0
  141. qaas_python-0.0.1/tests/adapters/test_github_vcs.py +554 -0
  142. qaas_python-0.0.1/tests/adapters/test_jira_tracker.py +1394 -0
  143. qaas_python-0.0.1/tests/conftest.py +54 -0
  144. qaas_python-0.0.1/tests/mcp/conftest.py +114 -0
  145. qaas_python-0.0.1/tests/mcp/test_contract_diff.py +587 -0
  146. qaas_python-0.0.1/tests/mcp/test_defect_memory.py +230 -0
  147. qaas_python-0.0.1/tests/mcp/test_env_control.py +334 -0
  148. qaas_python-0.0.1/tests/mcp/test_test_runner.py +339 -0
  149. qaas_python-0.0.1/tests/mcp/test_tracker.py +325 -0
  150. qaas_python-0.0.1/tests/mcp/test_vcs.py +344 -0
  151. qaas_python-0.0.1/tests/support.py +29 -0
  152. qaas_python-0.0.1/tests/target_app/test_seeded_defects.py +247 -0
  153. qaas_python-0.0.1/tests/test_cli.py +197 -0
  154. qaas_python-0.0.1/tests/test_config.py +239 -0
  155. qaas_python-0.0.1/tests/test_discover.py +141 -0
  156. qaas_python-0.0.1/tests/test_envelope.py +202 -0
  157. qaas_python-0.0.1/tests/test_envfile.py +101 -0
  158. qaas_python-0.0.1/tests/test_guardrails.py +603 -0
  159. qaas_python-0.0.1/tests/test_hooks_and_skills.py +242 -0
  160. qaas_python-0.0.1/tests/test_paths.py +163 -0
  161. qaas_python-0.0.1/tests/test_prompt_overrides.py +388 -0
  162. qaas_python-0.0.1/tests/test_registry.py +245 -0
  163. qaas_python-0.0.1/tests/test_router.py +630 -0
  164. qaas_python-0.0.1/tests/test_scorecard.py +438 -0
  165. qaas_python-0.0.1/tests/test_skills_actually_load.py +78 -0
  166. qaas_python-0.0.1/tests/test_store.py +192 -0
  167. qaas_python-0.0.1/tests/test_target_profile_is_honoured.py +149 -0
  168. qaas_python-0.0.1/tests/test_trace.py +473 -0
  169. qaas_python-0.0.1/tests/test_user_mcp_servers.py +152 -0
@@ -0,0 +1,24 @@
1
+ .qaas/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ node_modules/
7
+ .DS_Store
8
+ .env
9
+
10
+ # Run debris — agents write these while exploring; they are not source.
11
+ .playwright-mcp/
12
+ /*.png
13
+
14
+ # Agent worktrees — real git worktrees, never repo content.
15
+ .claude/worktrees/
16
+
17
+ # Build artifacts.
18
+ dist/
19
+
20
+ # Agent evidence screenshots land here because put_artifact takes only text.
21
+ target-app/*.png
22
+
23
+ # Personal notes, not published.
24
+ tutorial/
@@ -0,0 +1,476 @@
1
+ # How this system works
2
+
3
+ A guide to `qaas` for someone who has never opened it. Read this before
4
+ `CLAUDE.md` (which is terse notes for people already oriented) and before
5
+ `qa-agent-system-architecture.md` (which is the original design, not the code).
6
+
7
+ ---
8
+
9
+ ## 1. What it is, in one paragraph
10
+
11
+ `qaas` points a team of AI agents at an application, finds real defects in it,
12
+ reproduces them with runnable failing tests, files tickets, fixes some of them,
13
+ reviews the fixes, and verifies the fix actually worked. It is not a chatbot with
14
+ tools bolted on. It is a Python state machine that invokes agents the way a build
15
+ system invokes compilers: on a schedule, within a budget, with hard limits on
16
+ what each one may touch.
17
+
18
+ The thing it is pointed at is called the **target**. A bundled deliberately-buggy
19
+ demo app (`target-app/`) ships with it, along with a list of every bug seeded
20
+ into it (`target-app/defects.yaml`), so you can measure whether the agents
21
+ actually find things rather than take their word for it.
22
+
23
+ ---
24
+
25
+ ## 2. Entry point
26
+
27
+ There is exactly one:
28
+
29
+ ```toml
30
+ # pyproject.toml
31
+ [project.scripts]
32
+ qaas = "qaas.cli:app"
33
+ ```
34
+
35
+ `qaas` on the command line runs `src/qaas/cli.py`. Everything starts there. The
36
+ commands you will actually use:
37
+
38
+ | command | what it does | costs money? |
39
+ |---|---|---|
40
+ | `qaas validate` | checks config, prompts and allowlists are coherent | no |
41
+ | `qaas doctor --target corvid` | what this target makes possible | no |
42
+ | `qaas run --mode pr-check --dry-run` | renders each agent's exact options and prompt | no |
43
+ | `qaas run --mode pr-check` | **a real run** | yes |
44
+ | `qaas runs` / `qaas show <id>` | what happened in past runs | no |
45
+ | `qaas score` | recall/precision against the golden ledger | no |
46
+ | `qaas sweep` | run + score + fail below the quality gate — the cron entry | yes |
47
+ | `qaas tracker-check` | Jira auth/permissions preflight, creates nothing | no |
48
+
49
+ The tracker and vcs backends are `local` in the committed config and must stay
50
+ that way — the offline test suite builds real adapters, and a committed `jira`
51
+ breaks it. Switch per-shell instead:
52
+
53
+ ```bash
54
+ QAAS_TRACKER=jira qaas run --mode nightly # files to real Jira
55
+ QAAS_TRACKER=jira qaas tracker-check # preflight only
56
+ ```
57
+
58
+ Start with `qaas validate`, then `qaas run --dry-run`. Neither calls the API.
59
+
60
+ ---
61
+
62
+ ## 3. The shape of the code
63
+
64
+ ```
65
+ src/qaas/
66
+ ├── cli.py 866 entry point; every command above
67
+ ├── router.py 452 THE STATE MACHINE — phases, budget, concurrency, loops
68
+ ├── runner.py 118 invokes ONE agent, records what it cost
69
+ ├── registry.py 275 turns an agent's YAML into SDK options
70
+ ├── guardrails.py 413 the permission matrix, enforced in code
71
+ ├── envelope.py 263 DefectEnvelope — the only type agents pass each other
72
+ ├── store.py 225 run ledger, artifacts, versioned system map
73
+ ├── config.py 201 loads and validates config/
74
+ ├── tasks.py 349 builds the per-run task string for each agent
75
+ ├── target.py 249 target profiles — what makes it portable
76
+ ├── scorecard.py 424 measures recall/precision against the golden ledger
77
+ ├── discover.py 227 guesses a target profile for `qaas init`
78
+ ├── sdk_compat.py 52 fails loudly when the SDK changes hook names
79
+ ├── prompts/ one .md per agent + _shared.md appended to all
80
+ ├── mcp/ seven in-process tool servers (see §6)
81
+ └── adapters/ tracker (local | jira), vcs (local | github)
82
+ ```
83
+
84
+ If you read three files, read `router.py`, `envelope.py`, `guardrails.py`.
85
+ Everything else is in service of those.
86
+
87
+ ---
88
+
89
+ ## 4. The two decisions that explain everything else
90
+
91
+ **ROUTER is Python, not a prompt.** The original design has an orchestrator
92
+ agent. It is implemented as an ordinary state machine instead, because *a model
93
+ cannot enforce a budget it is itself spending*. Phase ordering, concurrency,
94
+ retries, escalation and the budget governor are all plain code in
95
+ `router.py`. This also makes runs reproducible and cheap to unit-test — 547
96
+ tests run offline with no API calls.
97
+
98
+ **Each agent is its own top-level `query()`.** Not SDK subagents of a shared
99
+ parent. That is what gives each agent a real context boundary, an enforceable
100
+ per-agent tool allowlist, and its own `total_cost_usd`. Nesting them would pool
101
+ cost into one number and blur the allowlist that the whole permission model
102
+ depends on.
103
+
104
+ ---
105
+
106
+ ## 5. The flow
107
+
108
+ ### 5.1 One run, end to end
109
+
110
+ ```
111
+ $ qaas run --mode full-loop
112
+ │
113
+ ▼
114
+ cli.py ── loads config/system.yaml + config/agents/*.yaml + the target profile
115
+ │
116
+ ▼
117
+ router.py ── runs five phases in order, in-process
118
+ │
119
+ ├── PHASE 1 map MAPPER → system-map.json (versioned, pinned)
120
+ ├── PHASE 2 discover API, BROWSER, … → DefectEnvelopes [concurrent]
121
+ ├── PHASE 3 reproduce REPRODUCER → failing test per finding
122
+ ├── PHASE 4 file TRIAGE → tickets
123
+ └── PHASE 5 verify VERIFIER ⇄ FIXER ⇄ REVIEWER [bounded loop]
124
+ ```
125
+
126
+ Each phase is a method on `Router`: `_phase_map`, `_phase_discover`,
127
+ `_phase_reproduce`, `_phase_file`, `_phase_verify`.
128
+
129
+ Which agents run is **config, not code** — `run_modes` in `config/system.yaml`:
130
+
131
+ ```yaml
132
+ pr-check: [MAPPER, API, BROWSER, REPRODUCER, TRIAGE] $16
133
+ nightly: [MAPPER, API, BROWSER, REPRODUCER, TRIAGE] $40
134
+ fix-cycle: [VERIFIER, FIXER, REVIEWER] $20
135
+ full-loop: all eight $60
136
+ ```
137
+
138
+ Note the shape: **REPRODUCER runs once per finding**, in a fresh context each time. So
139
+ cost scales with how much was found, not with how many agents exist.
140
+
141
+ ### 5.2 Dispatching one agent
142
+
143
+ ```
144
+ router._dispatch(spec, task)
145
+ │
146
+ ├── Budget.check() raises BudgetExceeded → a control working, not an error
147
+ │
148
+ ▼
149
+ runner.run_agent(spec, ctx, task)
150
+ │
151
+ ├── registry.build_options(spec, ctx)
152
+ │ ├── system prompt ← prompts/<AGENT>.md + prompts/_shared.md
153
+ │ ├── task string ← tasks.py (which app, which env, which finding)
154
+ │ ├── skills ← .claude/skills/*/SKILL.md
155
+ │ ├── mcp_servers ← in-process, closed over ToolContext
156
+ │ ├── allowed_tools ← this agent's allowlist only
157
+ │ └── hooks ← PreToolUse / PostToolUse / Stop
158
+ │
159
+ ▼
160
+ claude_agent_sdk.query(...) one top-level query, its own context
161
+ │
162
+ ▼
163
+ AgentResult cost_usd, num_turns, subtype, error → written to the run ledger
164
+ ```
165
+
166
+ Failures are **captured, not raised**. One agent falling over costs the run that
167
+ agent's findings, not the whole run; the router decides whether to retry, skip
168
+ or escalate.
169
+
170
+ ### 5.3 The remediation loop (phase 5)
171
+
172
+ This is the only phase with a cycle in it, so it is the only one that needs
173
+ explicit bounds:
174
+
175
+ ```
176
+ ┌─────────────────────────────────────────────┐
177
+ ▼ │
178
+ VERIFIER ──VERIFIED──▶ done │
179
+ │ │
180
+ ├──REGRESSED──▶ escalate to a human │
181
+ │ │
182
+ NOT_FIXED │
183
+ │ │
184
+ ├─ reopens ≥ max_proof_reopens ─▶ escalate │
185
+ ▼ │
186
+ FIXER ──▶ REVIEWER ──APPROVE────────────────────┘
187
+ │
188
+ ├─ REQUEST_CHANGES ─▶ back to FIXER (capped at 2 trips)
189
+ └─ ESCALATE_TO_HUMAN ─▶ stop
190
+ ```
191
+
192
+ The bound is the point. Without `max_proof_reopens`, a fix that keeps missing the
193
+ defect cycles until the budget is gone and the run ends with no verdict and no
194
+ money left to reach one. Escalating after one reopen costs a human five minutes;
195
+ not escalating costs the whole run.
196
+
197
+ VERIFIER's verdict is a **typed ledger entry** written through a tool
198
+ (`record_verdict`), never parsed out of the agent's prose.
199
+
200
+ ---
201
+
202
+ ## 6. Data: what actually moves between agents
203
+
204
+ ### 6.1 The DefectEnvelope is the only inter-agent type
205
+
206
+ Agents never pass prose to each other. `envelope.py` defines a Pydantic model
207
+ with `extra="forbid"`, and an agent's only write path is the
208
+ `envelope.emit_envelope` tool, which validates and rejects with field-level
209
+ errors.
210
+
211
+ ```
212
+ envelope_version id run_id discovered_by discovered_at
213
+ domain defect_class title summary
214
+ location evidence reproduction
215
+ impact severity confidence
216
+ suggested_owner suggested_fix_area autonomy_eligible
217
+ dedupe jira
218
+ ```
219
+
220
+ Two gates live on the model itself rather than in a prompt, because a prompt is a
221
+ request and a method is a rule:
222
+
223
+ - `has_evidence()` — an artifact or a failing test, or it is not a finding
224
+ - `is_fileable()` — evidence **and** confidence ≥ threshold **and** not
225
+ `not_reproducible`
226
+
227
+ `fingerprint()` deliberately **excludes** prose, line numbers, commit sha and
228
+ timestamps, so the same defect found twice by two differently-worded agents
229
+ hashes the same. That is what makes deduplication possible at all.
230
+
231
+ ### 6.2 Where state lives
232
+
233
+ ```
234
+ .qaas/ (gitignored — runtime state)
235
+ ├── runs/<run-id>/
236
+ │ ├── ledger.jsonl append-only: every tool call, denial, escalation
237
+ │ ├── envelopes/*.json the findings
238
+ │ ├── artifacts/ screenshots, logs, traces
239
+ │ └── results/*.json per-agent cost, turns, duration
240
+ ├── system-map/ versioned, shared across runs, pinned per run
241
+ ├── memory.db SQLite: defect fingerprints for dedupe
242
+ └── tickets/*.json the local tracker (when tracker: local)
243
+ ```
244
+
245
+ The ledger is the audit trail. `qaas show <run-id>` reads it. Evidence is
246
+ referenced by `artifact://<run>/<name>` URIs, and `resolve_artifact` rejects any
247
+ path escaping the store.
248
+
249
+ The system map is **versioned and pinned per run** so a bad map cannot
250
+ half-propagate through a run that already started.
251
+
252
+ ### 6.3 The seven MCP servers
253
+
254
+ All in-process (`create_sdk_mcp_server`), no subprocesses, no protocol
255
+ implementation. They close over a `ToolContext` holding the run store, config,
256
+ agent spec and pinned map version — so validation, guardrails and persistence
257
+ happen where the state already is.
258
+
259
+ | server | what it gives an agent |
260
+ |---|---|
261
+ | `envelope` | `emit_envelope`, `get_system_map`, `put_artifact` |
262
+ | `test_runner` | `run_suite`, `run_single`, `run_n_times`, `affected_tests` |
263
+ | `env_control` | `spin_up`, `seed`, `reset`, `set_flag`, `impersonate`, `tear_down` |
264
+ | `defect_memory` | `search_similar`, `fingerprint`, `record`, `get_occurrences` |
265
+ | `contract_diff` | `diff_openapi`, `classify_breaking`, `generate_contract_test` |
266
+ | `tracker` | `create_issue`, `transition`, `link`, `search` |
267
+ | `vcs` | `create_branch`, `write_file`, `commit`, `push`, `open_pull_request` |
268
+
269
+ Tool results use `ok()` / `err()` from `mcp/context.py`. Errors are **returned,
270
+ not raised**, so an agent reads the reason and corrects itself instead of dying.
271
+
272
+ Playwright is the one exception — a real stdio subprocess, declared in
273
+ `registry.STDIO_SERVERS`.
274
+
275
+ ---
276
+
277
+ ## 7. Agents are data, not code
278
+
279
+ An agent is **a prompt plus a YAML file**. Nothing else.
280
+
281
+ ```
282
+ src/qaas/prompts/API.md role, standards, what good looks like
283
+ config/agents/api.yaml model, budget, tools, skills, policy
284
+ ```
285
+
286
+ Adding an agent should require **no change** to `router.py`, `runner.py`,
287
+ `registry.py` or `guardrails.py`. A change to those files while adding an agent
288
+ is a sign something is wrong.
289
+
290
+ Where each kind of instruction belongs — this split matters and is easy to get
291
+ wrong:
292
+
293
+ | what | where | why |
294
+ |---|---|---|
295
+ | role, standards | `prompts/<AGENT>.md` | system prompt, stable across runs |
296
+ | house rules for all agents | `prompts/_shared.md` | appended to every prompt; not copy-pasted six times |
297
+ | **procedure** | `.claude/skills/<name>/SKILL.md` | loaded on demand, shared between agents |
298
+ | the per-run **task** | `tasks.py` | which app, which environment, which finding |
299
+ | **enforcement** | `guardrails.py` | a prompt is a request; code is a rule |
300
+
301
+ **Nothing in `tasks.py` or a prompt may name a specific application.** A prompt
302
+ that mentions one repo's layout or one app's seeded users works exactly once.
303
+
304
+ ### The sixteen agents
305
+
306
+ | agent | layer | does |
307
+ |---|---|---|
308
+ | MAPPER | map | services, routes, schema, ownership → `system-map.json` |
309
+ | ARCHITECT | discovery | circular deps, layering violations, god modules, dead code |
310
+ | API | discovery | API contract drift; ships a failing contract test |
311
+ | BROWSER | discovery | drives the UI through real journeys |
312
+ | DBA | discovery | schema constraints the code assumes and the database does not enforce |
313
+ | AUDITOR | discovery | missing authorization, secrets, vulnerable dependencies, leaked internals |
314
+ | SOCKET | discovery | WebSocket auth, reconnect, ordering, backpressure |
315
+ | GUIDE | discovery | whether a person can *find* a feature, not just whether it works |
316
+ | LOAD | discovery | N+1 queries, unindexed hot paths, unbounded results, bundle outliers |
317
+ | REPRODUCER | triage | reproduces, minimises, measures flake, commits a failing test |
318
+ | TRIAGE | triage | dedupes, scores severity, routes, files — the only tracker writer |
319
+ | FIXER | remediation | the minimal fix, on a `fix/*` branch |
320
+ | REVIEWER | remediation | adversarial review: APPROVE / REQUEST_CHANGES / ESCALATE |
321
+ | VERIFIER | verify | re-runs the original test → VERIFIED / NOT_FIXED / REGRESSED |
322
+ | REPORTER | reporting | what the run found, what recurred, and what it could not reach |
323
+
324
+ ROUTER is the sixteenth. It is the Python state machine in `router.py`
325
+ rather than an agent, because a model cannot enforce a budget it is spending.
326
+
327
+ ---
328
+
329
+ ## 8. Guardrails: how agents are actually constrained
330
+
331
+ The critical fact, learned the hard way in this repo:
332
+
333
+ > An `allowed_tools` entry naming a whole tool **auto-approves it before
334
+ > `can_use_tool` is consulted.** A policy implemented only in that callback is
335
+ > silently dead code.
336
+
337
+ So primary enforcement is the **`PreToolUse` hook**, with `can_use_tool` as a
338
+ second layer for calls the allowlist did not auto-approve. Both call one
339
+ `check()` function so they cannot disagree.
340
+
341
+ What is enforced:
342
+
343
+ - **Path scoping** — writes resolved and checked against `write_paths`. REPRODUCER and
344
+ FIXER only; everyone else denied.
345
+ - **Branch scoping** — git writes matched against branch patterns
346
+ (`qa/repro/*`, `fix/*`). `main` and force-push denied outright.
347
+ - **Ticket rate limit** — `max_tickets_per_run: 10`; over cap the call is denied
348
+ and the router escalates instead of filing.
349
+ - **Forbidden path classes** — migrations, auth, payment, secrets, infra, CI stop
350
+ at a human however small the change looks.
351
+ - **Diff budget** — counted per distinct file.
352
+ - **Immutable test** — FIXER may not edit the test recorded on the envelope.
353
+
354
+ Denials **return a reason and are logged**; they never kill the turn. The agent
355
+ reads the denial and adapts.
356
+
357
+ `ALWAYS_GRANTED` (ToolSearch, Skill, TodoWrite, Task, Agent) is read by both
358
+ `build_allowed_tools` and the guardrail — a mismatch there silently disables every
359
+ skill. Denying `ToolSearch` breaks MCP access entirely, since MCP tools arrive
360
+ deferred.
361
+
362
+ **Merge is impossible by construction.** No merge method exists anywhere in the
363
+ codebase, `gh pr merge` is refused by `FORBIDDEN_BASH`, and pull requests open as
364
+ drafts. Merging is a human decision.
365
+
366
+ ### Hooks enforce the output contract
367
+
368
+ - **`Stop`** blocks an agent that has not called its `must_call` tools, *while it
369
+ still has a turn to fix it* — the router would only find out afterwards. It
370
+ honours `stop_hook_active`; blocking twice burns budget.
371
+ - **`PostToolUse`** tells an agent immediately when an emitted envelope was
372
+ **held** rather than filed, since discovering that at the end is too late to
373
+ attach evidence.
374
+
375
+ ---
376
+
377
+ ## 9. Targets: why this is portable
378
+
379
+ `target.py` + `config/targets/*.yaml`. The load-bearing field is
380
+ `environment.mode`:
381
+
382
+ | mode | meaning |
383
+ |---|---|
384
+ | `none` | static reads only — no running app |
385
+ | `external` | exercise it, but never reset it |
386
+ | `compose` | own the lifecycle: spin up, seed, reset, tear down |
387
+
388
+ `profile.capabilities()` decides which agents can usefully run; `qaas doctor`
389
+ reports it. **Credentials are never in a profile** — it names environment
390
+ variables, and the values live outside the repo.
391
+
392
+ Pointing this at a different application is a new YAML file, not a code change.
393
+
394
+ ---
395
+
396
+ ## 10. Calibration: the part that keeps it honest
397
+
398
+ `target-app/` is deliberately buggy. `target-app/defects.yaml` is the **golden
399
+ ledger**: every seeded defect with the domain, severity and location a correct
400
+ agent should conclude. `scorecard.py` measures recall, precision, false-positive
401
+ rate and severity agreement against it.
402
+
403
+ ```
404
+ $ qaas score
405
+ recall 81% (13 of 16)
406
+ precision 100%
407
+ false positives 0
408
+ severity agreement 100%
409
+ cost per accepted $1.12
410
+ ```
411
+
412
+ The `not_defects` section plants correct-but-suspicious code, so **precision is
413
+ measured rather than assumed**.
414
+
415
+ Two rules:
416
+
417
+ 1. **Run `qaas score` after changing any prompt, threshold or model.** It is the
418
+ only way to know whether a change helped.
419
+ 2. **The ledger is not writable by the agents it scores** — deliberately. An
420
+ agent that can retire an entry can raise its own recall without fixing
421
+ anything. Retiring a repaired defect (`fixed_in: <ref>`) is a human's job at
422
+ merge time.
423
+
424
+ And the caveat that matters most: seeded defects are easier than real ones. Treat
425
+ 100% recall on `defects.yaml` as a floor, never as evidence the system is ready
426
+ for a real codebase.
427
+
428
+ ---
429
+
430
+ ## 11. Testing
431
+
432
+ ```bash
433
+ pytest # 547 tests, no API calls, no network, free
434
+ pytest -m docker # 18 tests, needs target-app running
435
+ pytest -m 'llm' # real API calls — excluded by default
436
+ ```
437
+
438
+ The markers `llm`, `docker`, `github`, `jira` are deselected by `addopts` in
439
+ `pyproject.toml`. **The default `pytest` run is offline and free, and must stay
440
+ that way.**
441
+
442
+ Four tiers, cheapest first:
443
+
444
+ 1. `pytest` — envelope, guardrails, MCP tools, adapters, scoring
445
+ 2. `qaas run --dry-run` — asserts assembled options match each allowlist
446
+ 3. `pytest -m llm` — one cheap live run per agent, asserting shape not content
447
+ 4. `qaas run && qaas score` — the honest number
448
+
449
+ ---
450
+
451
+ ## 12. Reading order for a newcomer
452
+
453
+ 1. `qaas validate` then `qaas run --mode pr-check --dry-run` — see the machine
454
+ describe itself, for free
455
+ 2. `envelope.py` — the contract everything else moves
456
+ 3. `router.py::run` — the five phases
457
+ 4. `config/agents/api.yaml` + `prompts/API.md` — what an agent *is*
458
+ 5. `guardrails.py::check` — the one function both enforcement points call
459
+ 6. `.qaas/runs/<id>/ledger.jsonl` from a real run — what actually happened
460
+
461
+ ---
462
+
463
+ ## 13. Known gaps
464
+
465
+ Honesty about what is *not* proven, so nobody inherits a false impression:
466
+
467
+ - **Eight of sixteen agents.** ARCHITECT, DBA, SOCKET, GUIDE, AUDITOR, LOAD and
468
+ REPORTER are designed but not built.
469
+ - **The extensibility claim is untested.** "A new agent needs only a prompt and a
470
+ YAML" is the architecture's central promise, and no one has added a ninth agent
471
+ to check it.
472
+ - **`fix-cycle` leaves the working tree on a `fix/*` branch.** Harmless when
473
+ watched; it would corrupt the next run of an unattended `qaas sweep` on cron.
474
+ - **Only ever run against the bundled demo app**, whose bugs it was told about.
475
+ - **`wont_fix` and `duplicate`** have no matching status in the connected Jira
476
+ workflow, so those transitions would fail.