qaas-python 0.1.0__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 (170) hide show
  1. qaas_python-0.1.0/.gitignore +15 -0
  2. qaas_python-0.1.0/ARCHITECTURE.md +469 -0
  3. qaas_python-0.1.0/BUILD_PLAN.md +306 -0
  4. qaas_python-0.1.0/CLAUDE.md +218 -0
  5. qaas_python-0.1.0/LICENSE +21 -0
  6. qaas_python-0.1.0/PKG-INFO +388 -0
  7. qaas_python-0.1.0/README.md +358 -0
  8. qaas_python-0.1.0/config/targets/corvid.yaml +56 -0
  9. qaas_python-0.1.0/pyproject.toml +79 -0
  10. qaas_python-0.1.0/src/qaas/adapters/__init__.py +19 -0
  11. qaas_python-0.1.0/src/qaas/adapters/tracker.py +1350 -0
  12. qaas_python-0.1.0/src/qaas/adapters/vcs.py +494 -0
  13. qaas_python-0.1.0/src/qaas/cli.py +1564 -0
  14. qaas_python-0.1.0/src/qaas/conductor.py +527 -0
  15. qaas_python-0.1.0/src/qaas/config.py +407 -0
  16. qaas_python-0.1.0/src/qaas/defaults/config/agents/arbiter.yaml +19 -0
  17. qaas_python-0.1.0/src/qaas/defaults/config/agents/cartographer.yaml +20 -0
  18. qaas_python-0.1.0/src/qaas/defaults/config/agents/clerk.yaml +21 -0
  19. qaas_python-0.1.0/src/qaas/defaults/config/agents/conduit.yaml +19 -0
  20. qaas_python-0.1.0/src/qaas/defaults/config/agents/forge.yaml +22 -0
  21. qaas_python-0.1.0/src/qaas/defaults/config/agents/mender.yaml +56 -0
  22. qaas_python-0.1.0/src/qaas/defaults/config/agents/proof.yaml +21 -0
  23. qaas_python-0.1.0/src/qaas/defaults/config/agents/surface.yaml +16 -0
  24. qaas_python-0.1.0/src/qaas/defaults/config/system.yaml +69 -0
  25. qaas_python-0.1.0/src/qaas/discover.py +227 -0
  26. qaas_python-0.1.0/src/qaas/envelope.py +290 -0
  27. qaas_python-0.1.0/src/qaas/guardrails.py +431 -0
  28. qaas_python-0.1.0/src/qaas/mcp/__init__.py +0 -0
  29. qaas_python-0.1.0/src/qaas/mcp/context.py +70 -0
  30. qaas_python-0.1.0/src/qaas/mcp/contract_diff.py +937 -0
  31. qaas_python-0.1.0/src/qaas/mcp/defect_memory.py +495 -0
  32. qaas_python-0.1.0/src/qaas/mcp/env_control.py +905 -0
  33. qaas_python-0.1.0/src/qaas/mcp/envelope_server.py +463 -0
  34. qaas_python-0.1.0/src/qaas/mcp/test_runner.py +773 -0
  35. qaas_python-0.1.0/src/qaas/mcp/tracker.py +412 -0
  36. qaas_python-0.1.0/src/qaas/mcp/vcs.py +506 -0
  37. qaas_python-0.1.0/src/qaas/paths.py +317 -0
  38. qaas_python-0.1.0/src/qaas/plugin/.claude-plugin/plugin.json +9 -0
  39. qaas_python-0.1.0/src/qaas/plugin/skills/a11y-audit/SKILL.md +34 -0
  40. qaas_python-0.1.0/src/qaas/plugin/skills/adversarial-review/SKILL.md +120 -0
  41. qaas_python-0.1.0/src/qaas/plugin/skills/api-surface-extraction/SKILL.md +38 -0
  42. qaas_python-0.1.0/src/qaas/plugin/skills/authz-matrix-check/SKILL.md +46 -0
  43. qaas_python-0.1.0/src/qaas/plugin/skills/console-error-triage/SKILL.md +39 -0
  44. qaas_python-0.1.0/src/qaas/plugin/skills/contract-test-generation/SKILL.md +36 -0
  45. qaas_python-0.1.0/src/qaas/plugin/skills/dedupe-strategy/SKILL.md +39 -0
  46. qaas_python-0.1.0/src/qaas/plugin/skills/environment-pinning/SKILL.md +35 -0
  47. qaas_python-0.1.0/src/qaas/plugin/skills/error-taxonomy/SKILL.md +42 -0
  48. qaas_python-0.1.0/src/qaas/plugin/skills/exploratory-ui-walk/SKILL.md +46 -0
  49. qaas_python-0.1.0/src/qaas/plugin/skills/failing-test-authoring/SKILL.md +47 -0
  50. qaas_python-0.1.0/src/qaas/plugin/skills/flake-detection/SKILL.md +39 -0
  51. qaas_python-0.1.0/src/qaas/plugin/skills/form-state-probe/SKILL.md +36 -0
  52. qaas_python-0.1.0/src/qaas/plugin/skills/minimal-diff-discipline/SKILL.md +70 -0
  53. qaas_python-0.1.0/src/qaas/plugin/skills/openapi-diff/SKILL.md +45 -0
  54. qaas_python-0.1.0/src/qaas/plugin/skills/ownership-resolution/SKILL.md +31 -0
  55. qaas_python-0.1.0/src/qaas/plugin/skills/product-task-graph/SKILL.md +35 -0
  56. qaas_python-0.1.0/src/qaas/plugin/skills/regression-risk-scoring/SKILL.md +59 -0
  57. qaas_python-0.1.0/src/qaas/plugin/skills/regression-suite-selection/SKILL.md +36 -0
  58. qaas_python-0.1.0/src/qaas/plugin/skills/repo-cartography/SKILL.md +38 -0
  59. qaas_python-0.1.0/src/qaas/plugin/skills/repro-minimisation/SKILL.md +41 -0
  60. qaas_python-0.1.0/src/qaas/plugin/skills/rollback-plan-authoring/SKILL.md +81 -0
  61. qaas_python-0.1.0/src/qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +67 -0
  62. qaas_python-0.1.0/src/qaas/plugin/skills/routing-rules/SKILL.md +34 -0
  63. qaas_python-0.1.0/src/qaas/plugin/skills/severity-rubric/SKILL.md +42 -0
  64. qaas_python-0.1.0/src/qaas/plugin/skills/test-first-fix/SKILL.md +66 -0
  65. qaas_python-0.1.0/src/qaas/plugin/skills/test-quality-audit/SKILL.md +58 -0
  66. qaas_python-0.1.0/src/qaas/plugin/skills/ticket-writer/SKILL.md +40 -0
  67. qaas_python-0.1.0/src/qaas/plugin/skills/verdict-reporting/SKILL.md +35 -0
  68. qaas_python-0.1.0/src/qaas/plugin/skills/verification-protocol/SKILL.md +39 -0
  69. qaas_python-0.1.0/src/qaas/prompts/ARBITER.md +53 -0
  70. qaas_python-0.1.0/src/qaas/prompts/CARTOGRAPHER.md +46 -0
  71. qaas_python-0.1.0/src/qaas/prompts/CLERK.md +45 -0
  72. qaas_python-0.1.0/src/qaas/prompts/CONDUIT.md +44 -0
  73. qaas_python-0.1.0/src/qaas/prompts/FORGE.md +43 -0
  74. qaas_python-0.1.0/src/qaas/prompts/MENDER.md +55 -0
  75. qaas_python-0.1.0/src/qaas/prompts/PROOF.md +41 -0
  76. qaas_python-0.1.0/src/qaas/prompts/SURFACE.md +46 -0
  77. qaas_python-0.1.0/src/qaas/prompts/_shared.md +45 -0
  78. qaas_python-0.1.0/src/qaas/registry.py +465 -0
  79. qaas_python-0.1.0/src/qaas/runner.py +192 -0
  80. qaas_python-0.1.0/src/qaas/scorecard.py +425 -0
  81. qaas_python-0.1.0/src/qaas/sdk_compat.py +52 -0
  82. qaas_python-0.1.0/src/qaas/store.py +290 -0
  83. qaas_python-0.1.0/src/qaas/target.py +261 -0
  84. qaas_python-0.1.0/src/qaas/tasks.py +361 -0
  85. qaas_python-0.1.0/src/qaas/trace.py +270 -0
  86. qaas_python-0.1.0/target-app/CODEOWNERS +15 -0
  87. qaas_python-0.1.0/target-app/api/Dockerfile +17 -0
  88. qaas_python-0.1.0/target-app/api/app/__init__.py +3 -0
  89. qaas_python-0.1.0/target-app/api/app/auth.py +113 -0
  90. qaas_python-0.1.0/target-app/api/app/config.py +38 -0
  91. qaas_python-0.1.0/target-app/api/app/db.py +36 -0
  92. qaas_python-0.1.0/target-app/api/app/errors.py +56 -0
  93. qaas_python-0.1.0/target-app/api/app/main.py +44 -0
  94. qaas_python-0.1.0/target-app/api/app/models.py +103 -0
  95. qaas_python-0.1.0/target-app/api/app/routes/__init__.py +5 -0
  96. qaas_python-0.1.0/target-app/api/app/routes/auth.py +31 -0
  97. qaas_python-0.1.0/target-app/api/app/routes/invoices.py +37 -0
  98. qaas_python-0.1.0/target-app/api/app/routes/orders.py +130 -0
  99. qaas_python-0.1.0/target-app/api/app/routes/stream.py +72 -0
  100. qaas_python-0.1.0/target-app/api/app/schemas.py +89 -0
  101. qaas_python-0.1.0/target-app/api/migrations/001_init.sql +61 -0
  102. qaas_python-0.1.0/target-app/api/pyproject.toml +22 -0
  103. qaas_python-0.1.0/target-app/api/seed/fixtures.sql +155 -0
  104. qaas_python-0.1.0/target-app/cross-tenant-order-detail-leak.png +0 -0
  105. qaas_python-0.1.0/target-app/defects.yaml +361 -0
  106. qaas_python-0.1.0/target-app/docker-compose.yml +48 -0
  107. qaas_python-0.1.0/target-app/new-order-validation-wipes-form.png +0 -0
  108. qaas_python-0.1.0/target-app/openapi.yaml +246 -0
  109. qaas_python-0.1.0/target-app/order-placed-banner-but-draft-status.png +0 -0
  110. qaas_python-0.1.0/target-app/place-order-noop-single-item.png +0 -0
  111. qaas_python-0.1.0/target-app/signout-secondary-button-contrast.png +0 -0
  112. qaas_python-0.1.0/target-app/web/.gitignore +3 -0
  113. qaas_python-0.1.0/target-app/web/Dockerfile +14 -0
  114. qaas_python-0.1.0/target-app/web/dist/assets/index-Cdavr0Eg.css +1 -0
  115. qaas_python-0.1.0/target-app/web/dist/assets/index-zpKS18v-.js +67 -0
  116. qaas_python-0.1.0/target-app/web/dist/index.html +13 -0
  117. qaas_python-0.1.0/target-app/web/index.html +12 -0
  118. qaas_python-0.1.0/target-app/web/package-lock.json +1792 -0
  119. qaas_python-0.1.0/target-app/web/package.json +24 -0
  120. qaas_python-0.1.0/target-app/web/src/api.ts +232 -0
  121. qaas_python-0.1.0/target-app/web/src/components/Button.tsx +24 -0
  122. qaas_python-0.1.0/target-app/web/src/components/Layout.tsx +53 -0
  123. qaas_python-0.1.0/target-app/web/src/components/SearchInput.tsx +24 -0
  124. qaas_python-0.1.0/target-app/web/src/main.tsx +74 -0
  125. qaas_python-0.1.0/target-app/web/src/routes/CheckoutReview.tsx +152 -0
  126. qaas_python-0.1.0/target-app/web/src/routes/Login.tsx +100 -0
  127. qaas_python-0.1.0/target-app/web/src/routes/NewOrder.tsx +258 -0
  128. qaas_python-0.1.0/target-app/web/src/routes/OrderDetail.tsx +179 -0
  129. qaas_python-0.1.0/target-app/web/src/routes/OrdersList.tsx +126 -0
  130. qaas_python-0.1.0/target-app/web/src/styles.css +430 -0
  131. qaas_python-0.1.0/target-app/web/tsconfig.json +21 -0
  132. qaas_python-0.1.0/target-app/web/vite.config.ts +28 -0
  133. qaas_python-0.1.0/tests/adapters/test_github_vcs.py +461 -0
  134. qaas_python-0.1.0/tests/adapters/test_jira_tracker.py +1048 -0
  135. qaas_python-0.1.0/tests/conftest.py +42 -0
  136. qaas_python-0.1.0/tests/mcp/conftest.py +114 -0
  137. qaas_python-0.1.0/tests/mcp/test_contract_diff.py +492 -0
  138. qaas_python-0.1.0/tests/mcp/test_defect_memory.py +230 -0
  139. qaas_python-0.1.0/tests/mcp/test_env_control.py +309 -0
  140. qaas_python-0.1.0/tests/mcp/test_test_runner.py +273 -0
  141. qaas_python-0.1.0/tests/mcp/test_tracker.py +325 -0
  142. qaas_python-0.1.0/tests/mcp/test_vcs.py +297 -0
  143. qaas_python-0.1.0/tests/support.py +29 -0
  144. qaas_python-0.1.0/tests/target_app/test_seeded_defects.py +247 -0
  145. qaas_python-0.1.0/tests/test_cli.py +196 -0
  146. qaas_python-0.1.0/tests/test_conductor.py +545 -0
  147. qaas_python-0.1.0/tests/test_config.py +219 -0
  148. qaas_python-0.1.0/tests/test_discover.py +115 -0
  149. qaas_python-0.1.0/tests/test_envelope.py +202 -0
  150. qaas_python-0.1.0/tests/test_guardrails.py +495 -0
  151. qaas_python-0.1.0/tests/test_hooks_and_skills.py +216 -0
  152. qaas_python-0.1.0/tests/test_paths.py +160 -0
  153. qaas_python-0.1.0/tests/test_prompt_overrides.py +388 -0
  154. qaas_python-0.1.0/tests/test_registry.py +187 -0
  155. qaas_python-0.1.0/tests/test_scorecard.py +403 -0
  156. qaas_python-0.1.0/tests/test_skills_actually_load.py +78 -0
  157. qaas_python-0.1.0/tests/test_store.py +122 -0
  158. qaas_python-0.1.0/tests/test_target_profile_is_honoured.py +149 -0
  159. qaas_python-0.1.0/tests/test_trace.py +376 -0
  160. qaas_python-0.1.0/tests/test_user_mcp_servers.py +152 -0
  161. qaas_python-0.1.0/tutorial/01-code-structure.md +241 -0
  162. qaas_python-0.1.0/tutorial/02-how-agents-communicate.md +963 -0
  163. qaas_python-0.1.0/tutorial/03-skills-and-hooks.md +607 -0
  164. qaas_python-0.1.0/tutorial/04-mcp-servers.md +709 -0
  165. qaas_python-0.1.0/tutorial/05-prompt-configuration.md +619 -0
  166. qaas_python-0.1.0/tutorial/06-jira-integration.md +938 -0
  167. qaas_python-0.1.0/tutorial/07-github-actions.md +499 -0
  168. qaas_python-0.1.0/tutorial/08-packaging-and-publishing.md +669 -0
  169. qaas_python-0.1.0/tutorial/09-guardrails-and-safety.md +1078 -0
  170. qaas_python-0.1.0/tutorial/README.md +88 -0
@@ -0,0 +1,15 @@
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/
@@ -0,0 +1,469 @@
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
+ ├── conductor.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 `conductor.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
+ **CONDUCTOR 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
+ `conductor.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
+ conductor.py ── runs five phases in order, in-process
118
+
119
+ ├── PHASE 1 map CARTOGRAPHER → system-map.json (versioned, pinned)
120
+ ├── PHASE 2 discover CONDUIT, SURFACE → DefectEnvelopes [concurrent]
121
+ ├── PHASE 3 reproduce FORGE → failing test per finding
122
+ ├── PHASE 4 file CLERK → tickets
123
+ └── PHASE 5 verify PROOF ⇄ MENDER ⇄ ARBITER [bounded loop]
124
+ ```
125
+
126
+ Each phase is a method on `Conductor`: `_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: [CARTOGRAPHER, CONDUIT, SURFACE, FORGE, CLERK] $16
133
+ nightly: [CARTOGRAPHER, CONDUIT, SURFACE, FORGE, CLERK] $40
134
+ fix-cycle: [PROOF, MENDER, ARBITER] $20
135
+ full-loop: all eight $60
136
+ ```
137
+
138
+ Note the shape: **FORGE 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
+ conductor._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 conductor 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
+ PROOF ──VERIFIED──▶ done │
179
+ │ │
180
+ ├──REGRESSED──▶ escalate to a human │
181
+ │ │
182
+ NOT_FIXED │
183
+ │ │
184
+ ├─ reopens ≥ max_proof_reopens ─▶ escalate │
185
+ ▼ │
186
+ MENDER ──▶ ARBITER ──APPROVE────────────────────┘
187
+
188
+ ├─ REQUEST_CHANGES ─▶ back to MENDER (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
+ PROOF'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/CONDUIT.md role, standards, what good looks like
283
+ config/agents/conduit.yaml model, budget, tools, skills, policy
284
+ ```
285
+
286
+ Adding an agent should require **no change** to `conductor.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 eight agents
305
+
306
+ | agent | layer | does |
307
+ |---|---|---|
308
+ | CARTOGRAPHER | map | services, routes, schema, ownership → `system-map.json` |
309
+ | CONDUIT | discovery | API contract drift; ships a failing contract test |
310
+ | SURFACE | discovery | drives the UI through real journeys |
311
+ | FORGE | triage | reproduces, minimises, measures flake, commits a failing test |
312
+ | CLERK | triage | dedupes, scores severity, routes, files — the only tracker writer |
313
+ | MENDER | remediation | the minimal fix, on a `fix/*` branch |
314
+ | ARBITER | remediation | adversarial review: APPROVE / REQUEST_CHANGES / ESCALATE |
315
+ | PROOF | verify | re-runs the original test → VERIFIED / NOT_FIXED / REGRESSED |
316
+
317
+ (The design doc names 16. Eight are built; CONDUCTOR is the Python state machine
318
+ rather than an agent; seven Phase-2 agents are not built.)
319
+
320
+ ---
321
+
322
+ ## 8. Guardrails: how agents are actually constrained
323
+
324
+ The critical fact, learned the hard way in this repo:
325
+
326
+ > An `allowed_tools` entry naming a whole tool **auto-approves it before
327
+ > `can_use_tool` is consulted.** A policy implemented only in that callback is
328
+ > silently dead code.
329
+
330
+ So primary enforcement is the **`PreToolUse` hook**, with `can_use_tool` as a
331
+ second layer for calls the allowlist did not auto-approve. Both call one
332
+ `check()` function so they cannot disagree.
333
+
334
+ What is enforced:
335
+
336
+ - **Path scoping** — writes resolved and checked against `write_paths`. FORGE and
337
+ MENDER only; everyone else denied.
338
+ - **Branch scoping** — git writes matched against branch patterns
339
+ (`qa/repro/*`, `fix/*`). `main` and force-push denied outright.
340
+ - **Ticket rate limit** — `max_tickets_per_run: 10`; over cap the call is denied
341
+ and the conductor escalates instead of filing.
342
+ - **Forbidden path classes** — migrations, auth, payment, secrets, infra, CI stop
343
+ at a human however small the change looks.
344
+ - **Diff budget** — counted per distinct file.
345
+ - **Immutable test** — MENDER may not edit the test recorded on the envelope.
346
+
347
+ Denials **return a reason and are logged**; they never kill the turn. The agent
348
+ reads the denial and adapts.
349
+
350
+ `ALWAYS_GRANTED` (ToolSearch, Skill, TodoWrite, Task, Agent) is read by both
351
+ `build_allowed_tools` and the guardrail — a mismatch there silently disables every
352
+ skill. Denying `ToolSearch` breaks MCP access entirely, since MCP tools arrive
353
+ deferred.
354
+
355
+ **Merge is impossible by construction.** No merge method exists anywhere in the
356
+ codebase, `gh pr merge` is refused by `FORBIDDEN_BASH`, and pull requests open as
357
+ drafts. Merging is a human decision.
358
+
359
+ ### Hooks enforce the output contract
360
+
361
+ - **`Stop`** blocks an agent that has not called its `must_call` tools, *while it
362
+ still has a turn to fix it* — the conductor would only find out afterwards. It
363
+ honours `stop_hook_active`; blocking twice burns budget.
364
+ - **`PostToolUse`** tells an agent immediately when an emitted envelope was
365
+ **held** rather than filed, since discovering that at the end is too late to
366
+ attach evidence.
367
+
368
+ ---
369
+
370
+ ## 9. Targets: why this is portable
371
+
372
+ `target.py` + `config/targets/*.yaml`. The load-bearing field is
373
+ `environment.mode`:
374
+
375
+ | mode | meaning |
376
+ |---|---|
377
+ | `none` | static reads only — no running app |
378
+ | `external` | exercise it, but never reset it |
379
+ | `compose` | own the lifecycle: spin up, seed, reset, tear down |
380
+
381
+ `profile.capabilities()` decides which agents can usefully run; `qaas doctor`
382
+ reports it. **Credentials are never in a profile** — it names environment
383
+ variables, and the values live outside the repo.
384
+
385
+ Pointing this at a different application is a new YAML file, not a code change.
386
+
387
+ ---
388
+
389
+ ## 10. Calibration: the part that keeps it honest
390
+
391
+ `target-app/` is deliberately buggy. `target-app/defects.yaml` is the **golden
392
+ ledger**: every seeded defect with the domain, severity and location a correct
393
+ agent should conclude. `scorecard.py` measures recall, precision, false-positive
394
+ rate and severity agreement against it.
395
+
396
+ ```
397
+ $ qaas score
398
+ recall 81% (13 of 16)
399
+ precision 100%
400
+ false positives 0
401
+ severity agreement 100%
402
+ cost per accepted $1.12
403
+ ```
404
+
405
+ The `not_defects` section plants correct-but-suspicious code, so **precision is
406
+ measured rather than assumed**.
407
+
408
+ Two rules:
409
+
410
+ 1. **Run `qaas score` after changing any prompt, threshold or model.** It is the
411
+ only way to know whether a change helped.
412
+ 2. **The ledger is not writable by the agents it scores** — deliberately. An
413
+ agent that can retire an entry can raise its own recall without fixing
414
+ anything. Retiring a repaired defect (`fixed_in: <ref>`) is a human's job at
415
+ merge time.
416
+
417
+ And the caveat that matters most: seeded defects are easier than real ones. Treat
418
+ 100% recall on `defects.yaml` as a floor, never as evidence the system is ready
419
+ for a real codebase.
420
+
421
+ ---
422
+
423
+ ## 11. Testing
424
+
425
+ ```bash
426
+ pytest # 547 tests, no API calls, no network, free
427
+ pytest -m docker # 18 tests, needs target-app running
428
+ pytest -m 'llm' # real API calls — excluded by default
429
+ ```
430
+
431
+ The markers `llm`, `docker`, `github`, `jira` are deselected by `addopts` in
432
+ `pyproject.toml`. **The default `pytest` run is offline and free, and must stay
433
+ that way.**
434
+
435
+ Four tiers, cheapest first:
436
+
437
+ 1. `pytest` — envelope, guardrails, MCP tools, adapters, scoring
438
+ 2. `qaas run --dry-run` — asserts assembled options match each allowlist
439
+ 3. `pytest -m llm` — one cheap live run per agent, asserting shape not content
440
+ 4. `qaas run && qaas score` — the honest number
441
+
442
+ ---
443
+
444
+ ## 12. Reading order for a newcomer
445
+
446
+ 1. `qaas validate` then `qaas run --mode pr-check --dry-run` — see the machine
447
+ describe itself, for free
448
+ 2. `envelope.py` — the contract everything else moves
449
+ 3. `conductor.py::run` — the five phases
450
+ 4. `config/agents/conduit.yaml` + `prompts/CONDUIT.md` — what an agent *is*
451
+ 5. `guardrails.py::check` — the one function both enforcement points call
452
+ 6. `.qaas/runs/<id>/ledger.jsonl` from a real run — what actually happened
453
+
454
+ ---
455
+
456
+ ## 13. Known gaps
457
+
458
+ Honesty about what is *not* proven, so nobody inherits a false impression:
459
+
460
+ - **Eight of sixteen agents.** KEYSTONE, VAULT, PULSE, USHER, WARDEN, GAUGE and
461
+ CHRONICLE are designed but not built.
462
+ - **The extensibility claim is untested.** "A new agent needs only a prompt and a
463
+ YAML" is the architecture's central promise, and no one has added a ninth agent
464
+ to check it.
465
+ - **`fix-cycle` leaves the working tree on a `fix/*` branch.** Harmless when
466
+ watched; it would corrupt the next run of an unattended `qaas sweep` on cron.
467
+ - **Only ever run against the bundled demo app**, whose bugs it was told about.
468
+ - **`wont_fix` and `duplicate`** have no matching status in the connected Jira
469
+ workflow, so those transitions would fail.