qaas-python 0.2.0__tar.gz → 0.2.2__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 (166) hide show
  1. {qaas_python-0.2.0 → qaas_python-0.2.2}/.gitignore +3 -0
  2. {qaas_python-0.2.0 → qaas_python-0.2.2}/PKG-INFO +1 -1
  3. {qaas_python-0.2.0 → qaas_python-0.2.2}/pyproject.toml +2 -2
  4. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/cli.py +6 -8
  5. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/conductor.py +22 -0
  6. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/target.py +18 -0
  7. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_conductor.py +20 -0
  8. qaas_python-0.2.0/tutorial/01-code-structure.md +0 -251
  9. qaas_python-0.2.0/tutorial/02-how-agents-communicate.md +0 -963
  10. qaas_python-0.2.0/tutorial/03-skills-and-hooks.md +0 -607
  11. qaas_python-0.2.0/tutorial/04-mcp-servers.md +0 -709
  12. qaas_python-0.2.0/tutorial/05-prompt-configuration.md +0 -619
  13. qaas_python-0.2.0/tutorial/06-jira-integration.md +0 -938
  14. qaas_python-0.2.0/tutorial/07-github-actions.md +0 -499
  15. qaas_python-0.2.0/tutorial/08-packaging-and-publishing.md +0 -669
  16. qaas_python-0.2.0/tutorial/09-guardrails-and-safety.md +0 -1078
  17. qaas_python-0.2.0/tutorial/README.md +0 -88
  18. {qaas_python-0.2.0 → qaas_python-0.2.2}/ARCHITECTURE.md +0 -0
  19. {qaas_python-0.2.0 → qaas_python-0.2.2}/BUILD_PLAN.md +0 -0
  20. {qaas_python-0.2.0 → qaas_python-0.2.2}/CLAUDE.md +0 -0
  21. {qaas_python-0.2.0 → qaas_python-0.2.2}/LICENSE +0 -0
  22. {qaas_python-0.2.0 → qaas_python-0.2.2}/README.md +0 -0
  23. {qaas_python-0.2.0 → qaas_python-0.2.2}/config/targets/corvid.yaml +0 -0
  24. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/adapters/__init__.py +0 -0
  25. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/adapters/tracker.py +0 -0
  26. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/adapters/vcs.py +0 -0
  27. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/config.py +0 -0
  28. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/arbiter.yaml +0 -0
  29. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/cartographer.yaml +0 -0
  30. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/clerk.yaml +0 -0
  31. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/conduit.yaml +0 -0
  32. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/forge.yaml +0 -0
  33. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/mender.yaml +0 -0
  34. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/proof.yaml +0 -0
  35. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/surface.yaml +0 -0
  36. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/vault.yaml +0 -0
  37. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/agents/warden.yaml +0 -0
  38. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/defaults/config/system.yaml +0 -0
  39. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/discover.py +0 -0
  40. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/envelope.py +0 -0
  41. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/guardrails.py +0 -0
  42. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/__init__.py +0 -0
  43. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/context.py +0 -0
  44. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/contract_diff.py +0 -0
  45. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/defect_memory.py +0 -0
  46. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/env_control.py +0 -0
  47. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/envelope_server.py +0 -0
  48. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/test_runner.py +0 -0
  49. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/tracker.py +0 -0
  50. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/mcp/vcs.py +0 -0
  51. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/paths.py +0 -0
  52. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/.claude-plugin/plugin.json +0 -0
  53. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/a11y-audit/SKILL.md +0 -0
  54. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/adversarial-review/SKILL.md +0 -0
  55. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/api-surface-extraction/SKILL.md +0 -0
  56. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/authz-matrix-check/SKILL.md +0 -0
  57. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/console-error-triage/SKILL.md +0 -0
  58. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/contract-test-generation/SKILL.md +0 -0
  59. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/dedupe-strategy/SKILL.md +0 -0
  60. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/environment-pinning/SKILL.md +0 -0
  61. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/error-taxonomy/SKILL.md +0 -0
  62. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/exploratory-ui-walk/SKILL.md +0 -0
  63. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/failing-test-authoring/SKILL.md +0 -0
  64. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/flake-detection/SKILL.md +0 -0
  65. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/form-state-probe/SKILL.md +0 -0
  66. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/minimal-diff-discipline/SKILL.md +0 -0
  67. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/openapi-diff/SKILL.md +0 -0
  68. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/ownership-resolution/SKILL.md +0 -0
  69. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/product-task-graph/SKILL.md +0 -0
  70. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/regression-risk-scoring/SKILL.md +0 -0
  71. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/regression-suite-selection/SKILL.md +0 -0
  72. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/repo-cartography/SKILL.md +0 -0
  73. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/repro-minimisation/SKILL.md +0 -0
  74. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/rollback-plan-authoring/SKILL.md +0 -0
  75. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +0 -0
  76. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/routing-rules/SKILL.md +0 -0
  77. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/severity-rubric/SKILL.md +0 -0
  78. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/test-first-fix/SKILL.md +0 -0
  79. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/test-quality-audit/SKILL.md +0 -0
  80. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/ticket-writer/SKILL.md +0 -0
  81. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/verdict-reporting/SKILL.md +0 -0
  82. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/plugin/skills/verification-protocol/SKILL.md +0 -0
  83. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/ARBITER.md +0 -0
  84. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/CARTOGRAPHER.md +0 -0
  85. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/CLERK.md +0 -0
  86. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/CONDUIT.md +0 -0
  87. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/FORGE.md +0 -0
  88. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/MENDER.md +0 -0
  89. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/PROOF.md +0 -0
  90. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/SURFACE.md +0 -0
  91. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/VAULT.md +0 -0
  92. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/WARDEN.md +0 -0
  93. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/prompts/_shared.md +0 -0
  94. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/registry.py +0 -0
  95. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/runner.py +0 -0
  96. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/scorecard.py +0 -0
  97. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/sdk_compat.py +0 -0
  98. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/store.py +0 -0
  99. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/tasks.py +0 -0
  100. {qaas_python-0.2.0 → qaas_python-0.2.2}/src/qaas/trace.py +0 -0
  101. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/CODEOWNERS +0 -0
  102. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/Dockerfile +0 -0
  103. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/__init__.py +0 -0
  104. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/auth.py +0 -0
  105. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/config.py +0 -0
  106. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/db.py +0 -0
  107. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/errors.py +0 -0
  108. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/main.py +0 -0
  109. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/models.py +0 -0
  110. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/routes/__init__.py +0 -0
  111. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/routes/auth.py +0 -0
  112. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/routes/invoices.py +0 -0
  113. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/routes/orders.py +0 -0
  114. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/routes/stream.py +0 -0
  115. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/app/schemas.py +0 -0
  116. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/migrations/001_init.sql +0 -0
  117. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/pyproject.toml +0 -0
  118. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/api/seed/fixtures.sql +0 -0
  119. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/defects.yaml +0 -0
  120. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/docker-compose.yml +0 -0
  121. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/openapi.yaml +0 -0
  122. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/.gitignore +0 -0
  123. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/Dockerfile +0 -0
  124. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/index.html +0 -0
  125. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/package-lock.json +0 -0
  126. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/package.json +0 -0
  127. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/api.ts +0 -0
  128. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/components/Button.tsx +0 -0
  129. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/components/Layout.tsx +0 -0
  130. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/components/SearchInput.tsx +0 -0
  131. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/main.tsx +0 -0
  132. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/routes/CheckoutReview.tsx +0 -0
  133. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/routes/Login.tsx +0 -0
  134. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/routes/NewOrder.tsx +0 -0
  135. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/routes/OrderDetail.tsx +0 -0
  136. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/routes/OrdersList.tsx +0 -0
  137. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/src/styles.css +0 -0
  138. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/tsconfig.json +0 -0
  139. {qaas_python-0.2.0 → qaas_python-0.2.2}/target-app/web/vite.config.ts +0 -0
  140. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/adapters/test_github_vcs.py +0 -0
  141. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/adapters/test_jira_tracker.py +0 -0
  142. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/conftest.py +0 -0
  143. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/conftest.py +0 -0
  144. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/test_contract_diff.py +0 -0
  145. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/test_defect_memory.py +0 -0
  146. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/test_env_control.py +0 -0
  147. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/test_test_runner.py +0 -0
  148. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/test_tracker.py +0 -0
  149. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/mcp/test_vcs.py +0 -0
  150. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/support.py +0 -0
  151. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/target_app/test_seeded_defects.py +0 -0
  152. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_cli.py +0 -0
  153. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_config.py +0 -0
  154. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_discover.py +0 -0
  155. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_envelope.py +0 -0
  156. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_guardrails.py +0 -0
  157. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_hooks_and_skills.py +0 -0
  158. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_paths.py +0 -0
  159. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_prompt_overrides.py +0 -0
  160. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_registry.py +0 -0
  161. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_scorecard.py +0 -0
  162. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_skills_actually_load.py +0 -0
  163. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_store.py +0 -0
  164. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_target_profile_is_honoured.py +0 -0
  165. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_trace.py +0 -0
  166. {qaas_python-0.2.0 → qaas_python-0.2.2}/tests/test_user_mcp_servers.py +0 -0
@@ -19,3 +19,6 @@ dist/
19
19
 
20
20
  # Agent evidence screenshots land here because put_artifact takes only text.
21
21
  target-app/*.png
22
+
23
+ # Personal notes, not published.
24
+ tutorial/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qaas-python
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A multi-agent QA system: finds real defects, reproduces them, files tickets, fixes them, and proves the fix
5
5
  Project-URL: Homepage, https://github.com/allaabdella2-us/qa-multi-agent-system
6
6
  Project-URL: Repository, https://github.com/allaabdella2-us/qa-multi-agent-system
@@ -2,7 +2,7 @@
2
2
  # Distribution name is `qaas-python` (`qaas` was taken); the import package and
3
3
  # the CLI are both `qaas`.
4
4
  name = "qaas-python"
5
- version = "0.2.0"
5
+ version = "0.2.2"
6
6
  description = "A multi-agent QA system: finds real defects, reproduces them, files tickets, fixes them, and proves the fix"
7
7
  readme = "README.md"
8
8
  license = "MIT"
@@ -63,7 +63,7 @@ include = [
63
63
  # (src/qaas/plugin/skills), so "src/" already carries them. The old
64
64
  # ".claude/skills/" entry outlived the directory it named -- hatchling
65
65
  # ignores a missing path, so it broke nothing and simply misled.
66
- "src/", "config/", "target-app/", "tests/", "tutorial/",
66
+ "src/", "config/", "target-app/", "tests/",
67
67
  "README.md", "LICENSE", "ARCHITECTURE.md", "CLAUDE.md", "BUILD_PLAN.md",
68
68
  "pyproject.toml",
69
69
  ]
@@ -449,16 +449,14 @@ def doctor(
449
449
 
450
450
 
451
451
  def _agent_usable(spec, caps: dict[str, bool]) -> bool:
452
- """Whether an agent can do useful work with the capabilities available.
452
+ """Delegates to `target.agent_usable`, which the conductor also uses.
453
453
 
454
- SURFACE without a browser-reachable UI has nothing to do; the rest can all
455
- contribute from static analysis alone, at lower confidence.
454
+ Two copies of this rule meant `qaas doctor` could report an agent unusable
455
+ while a run dispatched it anyway.
456
456
  """
457
- if spec.name == "SURFACE":
458
- return caps["live_ui"]
459
- if spec.name == "PROOF":
460
- return caps["live_api"] or caps["static_analysis"]
461
- return True
457
+ from qaas.target import agent_usable
458
+
459
+ return agent_usable(spec.name, caps)
462
460
 
463
461
 
464
462
  @app.command()
@@ -25,6 +25,7 @@ from typing import Any, Callable
25
25
 
26
26
  from qaas.config import AgentSpec, SystemConfig, load_config
27
27
  from qaas.envelope import DefectEnvelope
28
+ from qaas.target import agent_usable
28
29
  from qaas.mcp.context import ToolContext
29
30
  from qaas.runner import RunOutcome, run_agent
30
31
  from qaas.store import RunStore, SystemMapStore
@@ -269,6 +270,27 @@ class Conductor:
269
270
  async def _phase_discover(self, specs, store, budget, report, mode, map_version) -> None:
270
271
  """Discovery agents are independent. Run them concurrently, bounded."""
271
272
  discovery = [s for name, s in specs.items() if s.layer == "discovery"]
273
+
274
+ # Skip agents this target cannot support. `qaas doctor` has always
275
+ # reported these ("agents that cannot: SURFACE"), but nothing acted on
276
+ # it, so a run against a target with no reachable UI would still
277
+ # dispatch SURFACE and spend its entire budget hunting a browser that
278
+ # was never there. Being told an agent cannot work and then watching it
279
+ # run is worse than not being told.
280
+ profile = getattr(self.config, "profile", None)
281
+ if profile is not None:
282
+ caps = profile.capabilities()
283
+ unusable = [s for s in discovery if not agent_usable(s.name, caps)]
284
+ if unusable:
285
+ discovery = [s for s in discovery if s not in unusable]
286
+ store.log(
287
+ "skipped",
288
+ reason="target cannot support these agents",
289
+ agents=[s.name for s in unusable],
290
+ )
291
+ for spec in unusable:
292
+ self._emit("skipped", agent=spec.name, reason="target lacks the capability")
293
+
272
294
  if not discovery:
273
295
  return
274
296
 
@@ -259,3 +259,21 @@ def load_target(name: str, targets_dir: Path | str = "config/targets") -> Target
259
259
  # directory is the bug that hid `<project>/config/targets/` the moment anything
260
260
  # wrote into `.qaas/config/targets/`; profiles layer across every config
261
261
  # directory, and `config.target_files(dirs)` is the one place that knows it.
262
+
263
+
264
+ def agent_usable(agent_name: str, caps: dict[str, bool]) -> bool:
265
+ """Whether an agent can do useful work with the capabilities available.
266
+
267
+ Lives here, beside `capabilities()`, because it has two callers that must
268
+ agree: `qaas doctor` reports it, and the conductor acts on it. They did not
269
+ agree for a while -- doctor would say "agents that cannot: SURFACE" and then
270
+ a run would dispatch SURFACE anyway and spend its whole budget looking for a
271
+ browser that was never there. Being told an agent cannot work and then
272
+ watching it run is worse than not being told.
273
+
274
+ Everything except SURFACE can contribute from static analysis alone, at
275
+ lower confidence. SURFACE without a reachable UI has nothing to do at all.
276
+ """
277
+ if agent_name == "SURFACE":
278
+ return caps.get("live_ui", False)
279
+ return True
@@ -561,3 +561,23 @@ async def test_a_new_discovery_agent_runs_without_a_hand_written_task(cfg, tmp_p
561
561
  report = await make_conductor(cfg, tmp_path).run("nightly")
562
562
  ran = {name for name, _ in calls}
563
563
  assert {"VAULT", "WARDEN"} <= ran, f"a config-only agent was skipped: {sorted(ran)}"
564
+
565
+
566
+ async def test_an_agent_the_target_cannot_support_is_not_dispatched(cfg, tmp_path, fake_agents):
567
+ """`qaas doctor` has always reported "agents that cannot: SURFACE" for a
568
+ target with no reachable UI. Nothing acted on it: the run dispatched SURFACE
569
+ anyway and spent its budget looking for a browser that was never there.
570
+
571
+ Being told an agent cannot work and then watching it run is worse than not
572
+ being told, which is why this asserts the dispatch, not the report.
573
+ """
574
+ calls, behaviour = fake_agents
575
+ from qaas.target import Environment, TargetProfile
576
+
577
+ static_only = TargetProfile(name="static", root=".", environment=Environment(mode="none"))
578
+ cfg2 = cfg.model_copy(update={"profile": static_only})
579
+
580
+ await make_conductor(cfg2, tmp_path).run("pr-check")
581
+ ran = {name for name, _ in calls}
582
+ assert "SURFACE" not in ran, "dispatched SURFACE at a target with no UI"
583
+ assert "CONDUIT" in ran, "static analysis agents must still run"
@@ -1,251 +0,0 @@
1
- # 01 — Code structure and how the pieces relate
2
-
3
- Where every module lives, what it owns, and which module is allowed to know about
4
- which. Read this first; the other files assume it.
5
-
6
- ---
7
-
8
- ## The one-line version
9
-
10
- ```
11
- qaas <command> → cli.py → conductor.py → runner.py → claude_agent_sdk.query()
12
- ↑ ↑
13
- a state machine one agent, one call
14
- ```
15
-
16
- Everything else exists to serve those four files.
17
-
18
- ---
19
-
20
- ## The whole package
21
-
22
- ```
23
- src/qaas/
24
- ├── cli.py 1564 entry point — 13 commands
25
- ├── conductor.py 527 THE STATE MACHINE: phases, budget, concurrency, loops
26
- ├── runner.py 192 invokes ONE agent, records what it cost
27
- ├── registry.py 465 turns an agent's YAML into SDK options
28
- ├── guardrails.py 421 the permission matrix, enforced in code
29
- ├── config.py 407 loads and validates config/
30
- ├── tasks.py 361 builds the per-run task string for each agent
31
- ├── paths.py 317 where config, prompts and skills are found
32
- ├── store.py 290 run ledger, artifacts, versioned system map
33
- ├── envelope.py 290 DefectEnvelope — the only inter-agent type
34
- ├── trace.py 270 the read side of the ledger
35
- ├── target.py 261 target profiles — what makes it portable
36
- ├── scorecard.py 425 recall/precision against the golden ledger
37
- ├── discover.py 227 guesses a target profile for `qaas init`
38
- ├── sdk_compat.py 52 fails loudly when the SDK renames a hook event
39
-
40
- ├── prompts/ one .md per agent + _shared.md appended to all
41
- ├── plugin/ the skills, shipped as a Claude Code plugin
42
- │ ├── .claude-plugin/plugin.json
43
- │ └── skills/<30 dirs>/SKILL.md
44
- ├── defaults/config/ system.yaml + agents/*.yaml that ship in the wheel
45
- ├── mcp/ seven in-process tool servers
46
- └── adapters/ tracker (local | jira), vcs (local | github)
47
- ```
48
-
49
- Line counts are real, from `wc -l`. They are a rough guide to where the weight
50
- sits: `cli.py` and `adapters/tracker.py` are the two biggest files and are mostly
51
- surface area, not logic.
52
-
53
- ---
54
-
55
- ## The dependency graph
56
-
57
- This is the actual import graph, extracted from the source. **Arrows point at what
58
- a module is allowed to know about.** Nothing points back up.
59
-
60
- ```
61
- cli.py
62
- │ (config, paths, store, trace)
63
-
64
- conductor.py ────────────┐
65
- │ │
66
- (config, envelope, │ store, │
67
- mcp.context) ▼ │
68
- runner.py │
69
- │ (registry) │
70
- ▼ │
71
- registry.py │
72
- │ (config, │
73
- │ guardrails, │
74
- │ sdk_compat) │
75
- ▼ │
76
- guardrails.py │
77
- │ (mcp.context) │
78
- ▼ ▼
79
- config.py ────────► store.py
80
- │ (target) │ (envelope)
81
- ▼ ▼
82
- target.py ─────────► envelope.py
83
- │ (paths) (imports nothing)
84
-
85
- paths.py
86
- (imports nothing)
87
- ```
88
-
89
- Two facts fall out of this and are worth internalising:
90
-
91
- **`envelope.py` and `paths.py` import nothing from the package.** They are the
92
- foundation. `envelope.py` is the contract every agent speaks; `paths.py` is how
93
- the package finds its own resources. Both must be understandable on their own,
94
- and both are heavily tested in isolation.
95
-
96
- **`registry.py` imports `config.py`, so `config.py` cannot import `registry.py`.**
97
- There is a real consequence: the validator in `config.py` that checks every MCP
98
- server an agent names actually exists has to import `registry` *inside the
99
- function*. The comment there says so.
100
-
101
- ---
102
-
103
- ## What each module owns
104
-
105
- | module | owns | does **not** own |
106
- |---|---|---|
107
- | `cli.py` | argument parsing, printing, exit codes | any decision about a run |
108
- | `conductor.py` | phase order, budget, concurrency, escalation, loop bounds | how one agent runs |
109
- | `runner.py` | one `query()`, one `AgentResult` | whether that agent should run |
110
- | `registry.py` | assembling `ClaudeAgentOptions` | what the agent then does |
111
- | `guardrails.py` | allow/deny for every tool call | logging policy (it delegates to the store) |
112
- | `envelope.py` | the shape of a finding, and its two gates | where findings are stored |
113
- | `store.py` | the ledger, artifacts, the system map | what any entry means |
114
- | `config.py` | loading and validating YAML | where the YAML lives (that is `paths.py`) |
115
- | `paths.py` | config/prompt/skill/state resolution | reading any of them |
116
-
117
- ---
118
-
119
- ## The two decisions that explain the shape
120
-
121
- ### 1. CONDUCTOR is Python, not a prompt
122
-
123
- The design document describes an orchestrator agent. It is implemented as an
124
- ordinary state machine because **a model cannot enforce a budget it is itself
125
- spending.**
126
-
127
- Everything that must be true regardless of what a model decides lives in
128
- `conductor.py`: phase ordering, the concurrency cap, retries, escalation, and the
129
- loop bounds. That is also why 649 tests run offline with no API key — the
130
- interesting logic is not behind a model call.
131
-
132
- ```python
133
- # src/qaas/conductor.py — the five phases, in order
134
- map_version = await self._phase_map(specs, store, budget, report)
135
- await self._phase_discover(specs, store, budget, report, mode, map_version)
136
- await self._phase_reproduce(specs, store, budget, report, map_version)
137
- if run_mode.files_tickets:
138
- await self._phase_file(...)
139
- await self._phase_verify(...)
140
- ```
141
-
142
- ### 2. Every agent is its own top-level `query()`
143
-
144
- Not SDK subagents under a shared parent. Nesting would pool cost into one number
145
- and blur the per-agent tool allowlist that the whole permission model depends on.
146
-
147
- One `query()` per agent gives three things that matter:
148
-
149
- - a real context boundary — CONDUIT cannot see what SURFACE read
150
- - an enforceable allowlist — the tools are set per call, not per session
151
- - a per-agent `total_cost_usd`, straight off the `ResultMessage`
152
-
153
- `AgentDefinition` / `agents=` remains available for *intra-agent* fan-out.
154
-
155
- ---
156
-
157
- ## Agents are data, not code
158
-
159
- An agent is **a prompt plus a YAML file**. That is the whole definition.
160
-
161
- ```
162
- src/qaas/prompts/CONDUIT.md role and standards
163
- src/qaas/defaults/config/agents/conduit.yaml model, budget, tools, skills, policy
164
- ```
165
-
166
- Adding an agent should require **no change** to `conductor.py`, `runner.py`,
167
- `registry.py` or `guardrails.py`. A change to one of those while adding an agent
168
- is a sign something is wrong.
169
-
170
- Where each kind of instruction belongs — this split is easy to get wrong and the
171
- codebase is strict about it:
172
-
173
- | what | where | why |
174
- |---|---|---|
175
- | role, standards | `prompts/<AGENT>.md` | stable across every run |
176
- | house rules for all agents | `prompts/_shared.md` | appended to every prompt, not copy-pasted into eight |
177
- | **procedure** | `plugin/skills/<name>/SKILL.md` | loaded on demand, shared between agents |
178
- | the per-run **task** | `tasks.py` | which app, which environment, which finding |
179
- | **enforcement** | `guardrails.py` | a prompt is a request; code is a rule |
180
-
181
- > **Nothing in `tasks.py` or a prompt may name a specific application.** A prompt
182
- > that mentions one repo's layout works exactly once. This rule has been broken
183
- > and fixed: two `contract_diff` tool descriptions once said
184
- > `target-app/openapi.yaml` directly to the model.
185
-
186
- ---
187
-
188
- ## The eight agents
189
-
190
- | agent | layer | does |
191
- |---|---|---|
192
- | CARTOGRAPHER | map | services, routes, schema, ownership → `system-map.json` |
193
- | CONDUIT | discovery | API contract drift; ships a failing contract test |
194
- | SURFACE | discovery | drives the UI through real journeys |
195
- | VAULT | discovery | schema constraints the code assumes and the database does not enforce |
196
- | WARDEN | discovery | missing authorization, secrets, vulnerable dependencies, leaked internals |
197
- | FORGE | triage | reproduces, minimises, measures flake, commits a failing test |
198
- | CLERK | triage | dedupes, scores severity, routes, files — the only tracker writer |
199
- | MENDER | remediation | the minimal fix, on a `fix/*` branch |
200
- | ARBITER | remediation | adversarial review: APPROVE / REQUEST_CHANGES / ESCALATE |
201
- | PROOF | verify | re-runs the original test → VERIFIED / NOT_FIXED / REGRESSED |
202
-
203
- The design names 16. Ten are built; CONDUCTOR is the state machine rather than an
204
- agent; five further discovery specialists are designed and not written.
205
-
206
- VAULT and WARDEN are worth noting for *how* they were added: a prompt file and a
207
- YAML file each, with no Python. That is the architecture's central claim, and it
208
- was not quite true until they tested it -- `_phase_discover` dispatched from a
209
- closed dict of task builders, so a new discovery agent validated, assembled,
210
- appeared in `--dry-run`, and was then silently skipped with `no task builder`.
211
- There is a generic discovery task now, and a test that asserts a config-only
212
- agent actually *runs*.
213
-
214
- ---
215
-
216
- ## Runtime state
217
-
218
- ```
219
- .qaas/ (gitignored)
220
- ├── config/ your config, if you ran `qaas init`
221
- ├── runs/<run-id>/
222
- │ ├── ledger.jsonl append-only: every tool call, denial, escalation
223
- │ ├── envelopes/*.json the findings
224
- │ ├── artifacts/ screenshots, logs, traces
225
- │ └── results/*.json per-agent cost, turns, duration
226
- ├── system-map/ versioned, shared across runs, pinned per run
227
- ├── memory.db SQLite: defect fingerprints, for dedupe
228
- └── tickets/*.json the local tracker
229
- ```
230
-
231
- The ledger is not only an audit trail — the conductor **reads it back** for
232
- control flow. `_latest_verdict`, `_latest_review` and `_branch_written_since` all
233
- query it. That is unusual and it is deliberate: a verdict is a typed ledger entry,
234
- never parsed out of an agent's prose. See [02 — how agents
235
- communicate](02-how-agents-communicate.md).
236
-
237
- ---
238
-
239
- ## Reading order for a newcomer
240
-
241
- 1. `qaas validate` then `qaas run --mode pr-check --dry-run` — watch the machine
242
- describe itself, for free
243
- 2. `envelope.py` — the contract everything moves
244
- 3. `conductor.py::run` — the five phases
245
- 4. `defaults/config/agents/conduit.yaml` + `prompts/CONDUIT.md` — what an agent *is*
246
- 5. `guardrails.py::check` — the one function both enforcement points call
247
- 6. `.qaas/runs/<id>/ledger.jsonl` from a real run, or `qaas trace <id>`
248
-
249
- ---
250
-
251
- **Next:** [02 — How agents communicate](02-how-agents-communicate.md)