qaas-python 0.2.2__tar.gz → 0.2.3__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 (156) hide show
  1. {qaas_python-0.2.2 → qaas_python-0.2.3}/PKG-INFO +1 -1
  2. {qaas_python-0.2.2 → qaas_python-0.2.3}/pyproject.toml +1 -1
  3. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/cli.py +16 -23
  4. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/conductor.py +12 -7
  5. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/config.py +4 -2
  6. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/arbiter.yaml +0 -1
  7. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/cartographer.yaml +0 -1
  8. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/clerk.yaml +0 -1
  9. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/conduit.yaml +0 -1
  10. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/forge.yaml +0 -1
  11. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/mender.yaml +0 -1
  12. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/proof.yaml +0 -1
  13. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/surface.yaml +0 -1
  14. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/vault.yaml +0 -1
  15. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/agents/warden.yaml +0 -4
  16. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/defaults/config/system.yaml +5 -14
  17. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_conductor.py +16 -6
  18. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_config.py +19 -14
  19. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_paths.py +3 -3
  20. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_trace.py +19 -5
  21. {qaas_python-0.2.2 → qaas_python-0.2.3}/.gitignore +0 -0
  22. {qaas_python-0.2.2 → qaas_python-0.2.3}/ARCHITECTURE.md +0 -0
  23. {qaas_python-0.2.2 → qaas_python-0.2.3}/BUILD_PLAN.md +0 -0
  24. {qaas_python-0.2.2 → qaas_python-0.2.3}/CLAUDE.md +0 -0
  25. {qaas_python-0.2.2 → qaas_python-0.2.3}/LICENSE +0 -0
  26. {qaas_python-0.2.2 → qaas_python-0.2.3}/README.md +0 -0
  27. {qaas_python-0.2.2 → qaas_python-0.2.3}/config/targets/corvid.yaml +0 -0
  28. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/adapters/__init__.py +0 -0
  29. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/adapters/tracker.py +0 -0
  30. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/adapters/vcs.py +0 -0
  31. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/discover.py +0 -0
  32. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/envelope.py +0 -0
  33. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/guardrails.py +0 -0
  34. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/__init__.py +0 -0
  35. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/context.py +0 -0
  36. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/contract_diff.py +0 -0
  37. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/defect_memory.py +0 -0
  38. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/env_control.py +0 -0
  39. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/envelope_server.py +0 -0
  40. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/test_runner.py +0 -0
  41. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/tracker.py +0 -0
  42. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/mcp/vcs.py +0 -0
  43. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/paths.py +0 -0
  44. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/.claude-plugin/plugin.json +0 -0
  45. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/a11y-audit/SKILL.md +0 -0
  46. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/adversarial-review/SKILL.md +0 -0
  47. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/api-surface-extraction/SKILL.md +0 -0
  48. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/authz-matrix-check/SKILL.md +0 -0
  49. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/console-error-triage/SKILL.md +0 -0
  50. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/contract-test-generation/SKILL.md +0 -0
  51. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/dedupe-strategy/SKILL.md +0 -0
  52. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/environment-pinning/SKILL.md +0 -0
  53. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/error-taxonomy/SKILL.md +0 -0
  54. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/exploratory-ui-walk/SKILL.md +0 -0
  55. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/failing-test-authoring/SKILL.md +0 -0
  56. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/flake-detection/SKILL.md +0 -0
  57. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/form-state-probe/SKILL.md +0 -0
  58. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/minimal-diff-discipline/SKILL.md +0 -0
  59. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/openapi-diff/SKILL.md +0 -0
  60. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/ownership-resolution/SKILL.md +0 -0
  61. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/product-task-graph/SKILL.md +0 -0
  62. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/regression-risk-scoring/SKILL.md +0 -0
  63. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/regression-suite-selection/SKILL.md +0 -0
  64. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/repo-cartography/SKILL.md +0 -0
  65. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/repro-minimisation/SKILL.md +0 -0
  66. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/rollback-plan-authoring/SKILL.md +0 -0
  67. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +0 -0
  68. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/routing-rules/SKILL.md +0 -0
  69. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/severity-rubric/SKILL.md +0 -0
  70. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/test-first-fix/SKILL.md +0 -0
  71. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/test-quality-audit/SKILL.md +0 -0
  72. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/ticket-writer/SKILL.md +0 -0
  73. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/verdict-reporting/SKILL.md +0 -0
  74. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/plugin/skills/verification-protocol/SKILL.md +0 -0
  75. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/ARBITER.md +0 -0
  76. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/CARTOGRAPHER.md +0 -0
  77. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/CLERK.md +0 -0
  78. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/CONDUIT.md +0 -0
  79. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/FORGE.md +0 -0
  80. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/MENDER.md +0 -0
  81. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/PROOF.md +0 -0
  82. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/SURFACE.md +0 -0
  83. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/VAULT.md +0 -0
  84. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/WARDEN.md +0 -0
  85. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/prompts/_shared.md +0 -0
  86. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/registry.py +0 -0
  87. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/runner.py +0 -0
  88. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/scorecard.py +0 -0
  89. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/sdk_compat.py +0 -0
  90. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/store.py +0 -0
  91. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/target.py +0 -0
  92. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/tasks.py +0 -0
  93. {qaas_python-0.2.2 → qaas_python-0.2.3}/src/qaas/trace.py +0 -0
  94. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/CODEOWNERS +0 -0
  95. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/Dockerfile +0 -0
  96. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/__init__.py +0 -0
  97. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/auth.py +0 -0
  98. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/config.py +0 -0
  99. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/db.py +0 -0
  100. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/errors.py +0 -0
  101. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/main.py +0 -0
  102. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/models.py +0 -0
  103. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/routes/__init__.py +0 -0
  104. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/routes/auth.py +0 -0
  105. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/routes/invoices.py +0 -0
  106. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/routes/orders.py +0 -0
  107. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/routes/stream.py +0 -0
  108. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/app/schemas.py +0 -0
  109. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/migrations/001_init.sql +0 -0
  110. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/pyproject.toml +0 -0
  111. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/api/seed/fixtures.sql +0 -0
  112. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/defects.yaml +0 -0
  113. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/docker-compose.yml +0 -0
  114. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/openapi.yaml +0 -0
  115. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/.gitignore +0 -0
  116. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/Dockerfile +0 -0
  117. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/index.html +0 -0
  118. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/package-lock.json +0 -0
  119. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/package.json +0 -0
  120. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/api.ts +0 -0
  121. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/components/Button.tsx +0 -0
  122. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/components/Layout.tsx +0 -0
  123. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/components/SearchInput.tsx +0 -0
  124. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/main.tsx +0 -0
  125. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/routes/CheckoutReview.tsx +0 -0
  126. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/routes/Login.tsx +0 -0
  127. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/routes/NewOrder.tsx +0 -0
  128. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/routes/OrderDetail.tsx +0 -0
  129. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/routes/OrdersList.tsx +0 -0
  130. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/src/styles.css +0 -0
  131. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/tsconfig.json +0 -0
  132. {qaas_python-0.2.2 → qaas_python-0.2.3}/target-app/web/vite.config.ts +0 -0
  133. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/adapters/test_github_vcs.py +0 -0
  134. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/adapters/test_jira_tracker.py +0 -0
  135. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/conftest.py +0 -0
  136. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/conftest.py +0 -0
  137. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/test_contract_diff.py +0 -0
  138. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/test_defect_memory.py +0 -0
  139. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/test_env_control.py +0 -0
  140. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/test_test_runner.py +0 -0
  141. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/test_tracker.py +0 -0
  142. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/mcp/test_vcs.py +0 -0
  143. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/support.py +0 -0
  144. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/target_app/test_seeded_defects.py +0 -0
  145. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_cli.py +0 -0
  146. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_discover.py +0 -0
  147. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_envelope.py +0 -0
  148. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_guardrails.py +0 -0
  149. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_hooks_and_skills.py +0 -0
  150. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_prompt_overrides.py +0 -0
  151. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_registry.py +0 -0
  152. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_scorecard.py +0 -0
  153. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_skills_actually_load.py +0 -0
  154. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_store.py +0 -0
  155. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_target_profile_is_honoured.py +0 -0
  156. {qaas_python-0.2.2 → qaas_python-0.2.3}/tests/test_user_mcp_servers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qaas-python
3
- Version: 0.2.2
3
+ Version: 0.2.3
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.2"
5
+ version = "0.2.3"
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"
@@ -500,14 +500,18 @@ def validate(config_dir: Path | None = ConfigDir) -> None:
500
500
  # never dispatched. The mode meant for every pull request could not file a
501
501
  # ticket. It took a live run to notice; this check makes it free.
502
502
  for mode_name, mode in sorted(cfg.run_modes.items()):
503
- needed = sum(
504
- cfg.agents[a].max_budget_usd for a in mode.agents if a in cfg.agents
505
- )
506
- if needed > mode.max_budget_usd:
503
+ # Only meaningful when both sides declare a cap. The shipped config
504
+ # declares none, so this check simply does not fire there.
505
+ agent_caps = [
506
+ cfg.agents[a].max_budget_usd for a in mode.agents
507
+ if a in cfg.agents and cfg.agents[a].max_budget_usd is not None
508
+ ]
509
+ needed = sum(agent_caps)
510
+ if mode.max_budget_usd is not None and agent_caps and needed > mode.max_budget_usd:
507
511
  missing = [a for a in mode.agents if a in cfg.agents][-1]
508
512
  problems.append(
509
- f"mode '{mode_name}': agents can spend ${needed:.2f} but the cap is "
510
- f"${mode.max_budget_usd:.2f}, so the run stops before it reaches "
513
+ f"mode '{mode_name}': the agents' caps exceed the mode's cap, so "
514
+ f"its cap, so the run stops before it reaches "
511
515
  f"{missing} and files nothing. Raise max_budget_usd or drop an agent"
512
516
  )
513
517
 
@@ -567,7 +571,7 @@ def validate(config_dir: Path | None = ConfigDir) -> None:
567
571
  filing = "" if rm.files_tickets else " [dim](no filing)[/dim]"
568
572
  console.print(
569
573
  f"[bold]{mode}[/bold]: {', '.join(rm.agents)} "
570
- f"[dim]budget ${rm.max_budget_usd:.2f}, {rm.max_wall_clock_s}s[/dim]{filing}"
574
+ f"[dim]{rm.max_wall_clock_s}s[/dim]{filing}"
571
575
  )
572
576
 
573
577
  if notes:
@@ -834,7 +838,7 @@ def runs(root: Path = Root, limit: int = 10) -> None:
834
838
  console.print("[dim]no runs yet[/dim]")
835
839
  return
836
840
  table = Table(header_style="bold")
837
- for col in ("run", "envelopes", "agents", "cost"):
841
+ for col in ("run", "envelopes", "agents"):
838
842
  table.add_column(col)
839
843
  for run_id in ids:
840
844
  store = RunStore(run_id, root)
@@ -843,7 +847,6 @@ def runs(root: Path = Root, limit: int = 10) -> None:
843
847
  run_id,
844
848
  str(len(store.envelopes())),
845
849
  str(len(results)),
846
- f"${store.total_cost_usd():.2f}",
847
850
  )
848
851
  console.print(table)
849
852
 
@@ -872,9 +875,6 @@ def show(run_id: str, root: Path = Root) -> None:
872
875
  header.append(f"started {summary.started:%Y-%m-%d %H:%M:%S}Z")
873
876
  if summary.duration_s is not None:
874
877
  header.append(f"duration {summary.duration_s:.0f}s")
875
- header.append(f"cost ${summary.cost_usd:.2f}")
876
- if summary.budget_usd:
877
- header.append(f"of ${summary.budget_usd:.2f} budget")
878
878
  console.print(" " + " ".join(header))
879
879
  if summary.target_sha:
880
880
  dirty = " [yellow](dirty tree)[/yellow]" if summary.target_dirty else ""
@@ -960,7 +960,6 @@ def trace(
960
960
  table.add_column("agent", style="cyan")
961
961
  table.add_column("kind")
962
962
  table.add_column("detail", overflow="fold")
963
- table.add_column("cost", justify="right", style="dim")
964
963
  for row in trace_mod.timeline(entries):
965
964
  label = f"{row.kind} ×{row.count}" if row.count > 1 else row.kind
966
965
  table.add_row(
@@ -968,7 +967,6 @@ def trace(
968
967
  row.agent,
969
968
  f"[{KIND_STYLE.get(row.kind, 'white')}]{label}[/]",
970
969
  row.detail,
971
- f"${row.cost_usd:.2f}" if row.cost_usd is not None else "",
972
970
  )
973
971
  console.print(table)
974
972
  console.print(f"\n[dim]{len(entries)} entries[/dim]")
@@ -1082,7 +1080,7 @@ def run(
1082
1080
  rm = cfg.run_modes[mode]
1083
1081
  console.print(
1084
1082
  f"[bold]{mode}[/bold] — {len(specs)} agents, "
1085
- f"budget ${rm.max_budget_usd:.2f}, concurrency {rm.max_concurrency}"
1083
+ f"concurrency {rm.max_concurrency}"
1086
1084
  )
1087
1085
 
1088
1086
  if dry_run:
@@ -1093,7 +1091,7 @@ def run(
1093
1091
  d = describe(spec, prompt_dirs)
1094
1092
  console.print(
1095
1093
  f" [bold]{spec.name:14s}[/bold] {spec.model:18s} effort={spec.effort:7s} "
1096
- f"turns<={spec.max_turns:<3d} ${spec.max_budget_usd:.2f}"
1094
+ f"turns<={spec.max_turns}"
1097
1095
  )
1098
1096
  console.print(f" tools: {', '.join(d['allowed_tools'])}")
1099
1097
  console.print(f" prompt: {d['prompt_chars']} chars")
@@ -1101,11 +1099,11 @@ def run(
1101
1099
 
1102
1100
  def on_event(kind: str, detail: dict) -> None:
1103
1101
  if kind == "agent_started":
1104
- console.print(f"[dim]->[/dim] {detail.get('agent')} [dim](${detail.get('budget', 0):.2f})[/dim]")
1102
+ console.print(f"[dim]->[/dim] {detail.get('agent')}")
1105
1103
  elif kind == "finished":
1106
1104
  console.print(
1107
1105
  f"[dim]<-[/dim] {detail.get('agent')} "
1108
- f"[dim]${detail.get('cost', 0):.3f}, {detail.get('envelopes', 0)} findings[/dim]"
1106
+ f"[dim]{detail.get('envelopes', 0)} findings[/dim]"
1109
1107
  )
1110
1108
  elif kind == "stopped":
1111
1109
  console.print(f"[yellow]stopped: {detail.get('reason')}[/yellow]")
@@ -1172,11 +1170,6 @@ def score(
1172
1170
  table.add_row("false positives", f"{s['false_positives']} ({s['false_positive_rate']:.0%})")
1173
1171
  table.add_row("duplicates", f"{s['duplicates']} ({s['duplicate_rate']:.0%})")
1174
1172
  table.add_row("severity agreement", f"{s['severity_agreement']:.0%}")
1175
- table.add_row("cost", f"${s['cost_usd']:.2f}")
1176
- table.add_row(
1177
- "cost per accepted",
1178
- f"${s['cost_per_accepted']:.2f}" if s["cost_per_accepted"] is not None else "-",
1179
- )
1180
1173
  console.print(table)
1181
1174
 
1182
1175
  if card.matches:
@@ -110,7 +110,7 @@ class RunReport:
110
110
  class Budget:
111
111
  """The spend and wall-clock governor. Checked before every dispatch."""
112
112
 
113
- def __init__(self, max_usd: float, max_seconds: int, *, already_spent: float = 0.0):
113
+ def __init__(self, max_usd: float | None, max_seconds: int, *, already_spent: float = 0.0):
114
114
  self.max_usd = max_usd
115
115
  self.max_seconds = max_seconds
116
116
  #: What this run has already cost, including earlier invocations.
@@ -130,23 +130,28 @@ class Budget:
130
130
  return time.monotonic() - self.started
131
131
 
132
132
  @property
133
- def remaining_usd(self) -> float:
134
- return max(0.0, self.max_usd - self.spent)
133
+ def remaining_usd(self) -> float | None:
134
+ return None if self.max_usd is None else max(0.0, self.max_usd - self.spent)
135
135
 
136
136
  def spend(self, amount: float) -> None:
137
137
  self.spent += amount
138
138
 
139
139
  def check(self) -> None:
140
- if self.spent >= self.max_usd:
140
+ # `max_usd is None` means no spend ceiling -- the shipped config sets
141
+ # none, because a dollar figure bakes one vendor's pricing into a tool
142
+ # meant to run against local models too. The wall-clock cap and each
143
+ # agent's `max_turns` still bound a run; those are model-agnostic.
144
+ if self.max_usd is not None and self.spent >= self.max_usd:
141
145
  raise BudgetExceeded(f"spend cap reached: ${self.spent:.2f} of ${self.max_usd:.2f}")
142
146
  if self.elapsed >= self.max_seconds:
143
147
  raise BudgetExceeded(
144
148
  f"wall-clock cap reached: {self.elapsed:.0f}s of {self.max_seconds}s"
145
149
  )
146
150
 
147
- def allowance(self, spec: AgentSpec) -> float:
148
- """What this agent may spend: its own cap, or what the run has left."""
149
- return max(0.01, min(spec.max_budget_usd, self.remaining_usd))
151
+ def allowance(self, spec: AgentSpec) -> float | None:
152
+ """What this agent may spend, or None when neither it nor the run caps it."""
153
+ caps = [c for c in (spec.max_budget_usd, self.remaining_usd) if c is not None]
154
+ return max(0.01, min(caps)) if caps else None
150
155
 
151
156
 
152
157
  class Conductor:
@@ -74,7 +74,8 @@ class AgentSpec(BaseModel):
74
74
  model: str = "claude-opus-5"
75
75
  effort: Literal["low", "medium", "high", "xhigh", "max"] = "high"
76
76
  max_turns: int = 40
77
- max_budget_usd: float = 2.0
77
+ max_budget_usd: float | None = None
78
+
78
79
 
79
80
  mcp_servers: list[str] = Field(default_factory=list)
80
81
  builtin_tools: list[str] = Field(default_factory=list)
@@ -165,7 +166,8 @@ class RunMode(BaseModel):
165
166
 
166
167
  trigger: str
167
168
  agents: list[str]
168
- max_budget_usd: float = 10.0
169
+ max_budget_usd: float | None = None
170
+
169
171
  max_wall_clock_s: int = 3600
170
172
  max_concurrency: int = 3
171
173
  files_tickets: bool = True
@@ -9,7 +9,6 @@ prompt: ARBITER.md
9
9
  model: claude-opus-5
10
10
  effort: high
11
11
  max_turns: 50
12
- max_budget_usd: 3.0
13
12
  mcp_servers: [envelope, vcs, contract_diff, test_runner]
14
13
  builtin_tools: [Read, Grep, Glob]
15
14
  skills: [adversarial-review, root-cause-vs-symptom, regression-risk-scoring, test-quality-audit]
@@ -7,7 +7,6 @@ prompt: CARTOGRAPHER.md
7
7
  model: claude-sonnet-5 # extraction, not judgment
8
8
  effort: medium
9
9
  max_turns: 60
10
- max_budget_usd: 2.0
11
10
  mcp_servers: [envelope]
12
11
  builtin_tools: [Read, Grep, Glob]
13
12
  policy: {} # read-only
@@ -8,7 +8,6 @@ prompt: CLERK.md
8
8
  model: claude-sonnet-5 # composition against a fixed rubric and house format
9
9
  effort: high
10
10
  max_turns: 50
11
- max_budget_usd: 2.0
12
11
  mcp_servers: [envelope, tracker, defect_memory]
13
12
  builtin_tools: [Read]
14
13
  policy:
@@ -8,7 +8,6 @@ prompt: CONDUIT.md
8
8
  model: claude-opus-5
9
9
  effort: high
10
10
  max_turns: 60
11
- max_budget_usd: 3.0
12
11
  mcp_servers: [envelope, contract_diff, env_control, defect_memory]
13
12
  builtin_tools: [Read, Grep, Glob]
14
13
  policy: {}
@@ -8,7 +8,6 @@ prompt: FORGE.md
8
8
  model: claude-opus-5
9
9
  effort: high
10
10
  max_turns: 80
11
- max_budget_usd: 4.0
12
11
  mcp_servers: [envelope, test_runner, env_control, vcs]
13
12
  builtin_tools: [Read, Grep, Glob, Write, Edit, Bash]
14
13
  policy:
@@ -10,7 +10,6 @@ prompt: MENDER.md
10
10
  model: claude-opus-5
11
11
  effort: high
12
12
  max_turns: 80
13
- max_budget_usd: 5.0
14
13
  mcp_servers: [envelope, test_runner, env_control, vcs, tracker, contract_diff]
15
14
  builtin_tools: [Read, Grep, Glob, Write, Edit, Bash]
16
15
  skills: [test-first-fix, minimal-diff-discipline, root-cause-vs-symptom, rollback-plan-authoring]
@@ -8,7 +8,6 @@ prompt: PROOF.md
8
8
  model: claude-opus-5
9
9
  effort: high
10
10
  max_turns: 60
11
- max_budget_usd: 4.0
12
11
  mcp_servers: [envelope, test_runner, env_control, tracker, vcs]
13
12
  builtin_tools: [Read, Grep, Glob, Bash]
14
13
  policy:
@@ -8,7 +8,6 @@ prompt: SURFACE.md
8
8
  model: claude-opus-5
9
9
  effort: high
10
10
  max_turns: 80
11
- max_budget_usd: 4.0
12
11
  mcp_servers: [envelope, env_control, playwright]
13
12
  builtin_tools: [Read, Grep, Glob]
14
13
  policy: {}
@@ -10,7 +10,6 @@ prompt: VAULT.md
10
10
  model: claude-opus-5
11
11
  effort: high
12
12
  max_turns: 60
13
- max_budget_usd: 3.0
14
13
  mcp_servers: [envelope, env_control, defect_memory]
15
14
  builtin_tools: [Read, Grep, Glob]
16
15
  policy: {}
@@ -9,10 +9,6 @@ prompt: WARDEN.md
9
9
  model: claude-opus-5
10
10
  effort: high
11
11
  max_turns: 60
12
- # Measured: WARDEN exhausted $3.00 on its first real run against the demo app
13
- # and was killed mid-audit. Building the endpoint-by-role matrix and actually
14
- # impersonating each role costs more than reading a spec does.
15
- max_budget_usd: 5.0
16
12
  mcp_servers: [envelope, env_control, defect_memory]
17
13
  builtin_tools: [Read, Grep, Glob]
18
14
  policy: {}
@@ -9,6 +9,11 @@ target:
9
9
  tracker: local # local | jira -- swap to file against real Jira
10
10
  vcs: local # local | github
11
11
 
12
+ # No spend cap by default. A dollar figure bakes one vendor's pricing into
13
+ # the config, and this is meant to run against local models too. `max_turns`
14
+ # is the model-agnostic bound. Set `max_budget_usd` here if you want a
15
+ # ceiling; the governor enforces one whenever it is present.
16
+
12
17
  thresholds:
13
18
  min_confidence_to_file: 0.6 # §7 confidence gate
14
19
  max_findings_per_agent_run: 25 # §8.3 loop breaker: pause and escalate, don't file
@@ -21,30 +26,18 @@ run_modes:
21
26
  pr-check:
22
27
  trigger: pull_request
23
28
  agents: [CARTOGRAPHER, CONDUIT, SURFACE, FORGE, CLERK]
24
- # Was 6.0, which this roster could not complete. Measured on a real run:
25
- # CARTOGRAPHER $0.69 + CONDUIT $2.25 + SURFACE $3.16 = $6.09, so the governor
26
- # stopped the run before FORGE or CLERK ever dispatched. The mode meant to
27
- # run on every pull request could not file a ticket, and it failed silently:
28
- # agents ran, findings landed in the ledger, nothing errored. `qaas validate`
29
- # now refuses a mode whose agents cannot fit inside its cap.
30
- max_budget_usd: 16.0
31
29
  max_wall_clock_s: 900
32
30
  max_concurrency: 2
33
31
 
34
32
  nightly:
35
33
  trigger: cron
36
34
  agents: [CARTOGRAPHER, CONDUIT, SURFACE, VAULT, WARDEN, FORGE, CLERK]
37
- # FORGE runs once per finding, so the deep sweep's budget scales with how
38
- # much discovery found, not with the number of agents. Measured: discovery
39
- # ~$6, then roughly $1-2 per finding reproduced.
40
- max_budget_usd: 50.0
41
35
  max_wall_clock_s: 7200
42
36
  max_concurrency: 3
43
37
 
44
38
  incident:
45
39
  trigger: alert
46
40
  agents: [CONDUIT]
47
- max_budget_usd: 4.0
48
41
  max_wall_clock_s: 600
49
42
  max_concurrency: 2
50
43
  files_tickets: false # §9: diagnostic only, read-only, no filing
@@ -55,7 +48,6 @@ run_modes:
55
48
  fix-cycle:
56
49
  trigger: agent_ready_ticket
57
50
  agents: [PROOF, MENDER, ARBITER]
58
- max_budget_usd: 20.0
59
51
  max_wall_clock_s: 3600
60
52
  max_concurrency: 1
61
53
 
@@ -64,6 +56,5 @@ run_modes:
64
56
  full-loop:
65
57
  trigger: on_demand
66
58
  agents: [CARTOGRAPHER, CONDUIT, SURFACE, VAULT, WARDEN, FORGE, CLERK, MENDER, ARBITER, PROOF]
67
- max_budget_usd: 70.0
68
59
  max_wall_clock_s: 10800
69
60
  max_concurrency: 3
@@ -168,10 +168,18 @@ async def test_each_forge_invocation_gets_its_own_finding(cfg, tmp_path, fake_ag
168
168
 
169
169
 
170
170
  async def test_a_spend_blowout_stops_the_run_and_escalates(cfg, tmp_path, fake_agents):
171
+ """The governor still works; it just no longer ships with a dollar figure.
172
+
173
+ Shipped config sets no `max_budget_usd` -- a price belongs to one vendor and
174
+ this runs against local models too -- so a test about spend has to declare
175
+ the cap it is testing.
176
+ """
171
177
  calls, behaviour = fake_agents
172
178
  behaviour["CARTOGRAPHER"] = {"publish_map": True, "cost": 99.0}
173
179
 
174
- report = await make_conductor(cfg, tmp_path).run("pr-check")
180
+ mode = cfg.run_modes["pr-check"].model_copy(update={"max_budget_usd": 10.0})
181
+ capped = cfg.model_copy(update={"run_modes": {**cfg.run_modes, "pr-check": mode}})
182
+ report = await make_conductor(capped, tmp_path).run("pr-check")
175
183
  assert report.stopped_early and "spend cap" in report.stopped_early
176
184
  assert "CLERK" not in [n for n, _ in calls]
177
185
  assert report.escalations
@@ -533,13 +541,15 @@ async def test_a_resumed_run_keeps_the_budget_it_already_spent(cfg, tmp_path, fa
533
541
  store = RunStore.new(tmp_path)
534
542
 
535
543
  # A first invocation that has already consumed most of the mode's cap.
536
- mode = cfg.run_modes["pr-check"]
537
- store.put_result(AgentResult(agent="CONDUIT", subtype="success",
538
- cost_usd=mode.max_budget_usd - 0.05, num_turns=1))
539
- assert store.total_cost_usd() == pytest.approx(mode.max_budget_usd - 0.05)
544
+ capped_mode = cfg.run_modes["pr-check"].model_copy(update={"max_budget_usd": 5.0})
545
+ capped = cfg.model_copy(update={"run_modes": {**cfg.run_modes, "pr-check": capped_mode}})
546
+ store.put_result(AgentResult(agent="CONDUIT", subtype="success", cost_usd=4.95, num_turns=1))
547
+ assert store.total_cost_usd() == pytest.approx(4.95)
540
548
 
541
549
  behaviour["CARTOGRAPHER"] = {"cost": 1.0, "publish_map": True}
542
- report = await make_conductor(cfg, tmp_path).run("pr-check", run_id=store.run_id)
550
+ # The shipped config sets no cap, so this test supplies one -- the governor
551
+ # is still there, it just no longer bakes a dollar figure into defaults.
552
+ report = await make_conductor(capped, tmp_path).run("pr-check", run_id=store.run_id)
543
553
 
544
554
  assert report.stopped_early, "the resumed run ignored what the run had already spent"
545
555
  assert "spend cap" in report.stopped_early
@@ -121,12 +121,21 @@ def test_incident_mode_files_nothing(cfg):
121
121
  assert "CLERK" not in incident.agents
122
122
 
123
123
 
124
- def test_every_run_mode_is_budgeted(cfg):
124
+ def test_every_run_mode_is_bounded(cfg):
125
+ """Every mode must have SOME bound. It used to be a dollar cap; the shipped
126
+ config sets none now, because a price belongs to one vendor and this is
127
+ meant to run against local models too. The wall clock is the bound that
128
+ survives that, and `max_turns` bounds each agent."""
125
129
  for name, mode in cfg.run_modes.items():
126
- assert mode.max_budget_usd > 0, name
127
- assert mode.max_wall_clock_s > 0, name
130
+ assert mode.max_wall_clock_s > 0, f"{name} has no wall-clock bound"
131
+ assert mode.max_concurrency > 0, f"{name} has no concurrency bound"
128
132
 
129
133
 
134
+ def test_no_vendor_pricing_is_baked_into_the_shipped_config(cfg):
135
+ """A dollar figure in the defaults is a bet on one provider's price list."""
136
+ assert all(s.max_budget_usd is None for s in cfg.agents.values())
137
+ assert all(m.max_budget_usd is None for m in cfg.run_modes.values())
138
+
130
139
  def test_enabled_agents_resolves_a_mode(cfg):
131
140
  names = [s.name for s in cfg.enabled_agents("pr-check")]
132
141
  assert names == ["CARTOGRAPHER", "CONDUIT", "SURFACE", "FORGE", "CLERK"]
@@ -209,16 +218,12 @@ def test_a_nonsense_override_is_rejected_loudly(monkeypatch):
209
218
  load_config(search=CONFIG_SEARCH)
210
219
 
211
220
 
212
- def test_every_mode_can_afford_the_agents_it_names():
213
- """A mode whose roster outspends its cap stops partway through and looks
214
- like it worked. `pr-check` shipped with a $6 cap against a $15 roster:
215
- discovery alone spent $6.09, so FORGE and CLERK never dispatched and the
216
- mode meant for every pull request could not file a ticket. Nothing errored,
217
- which is what made it survive."""
221
+ def test_a_mode_that_cannot_afford_its_agents_is_rejected():
222
+ """Still enforced -- but only when someone actually sets caps, since the
223
+ shipped config sets none."""
218
224
  cfg = load_config(search=CONFIG_SEARCH)
219
225
  for name, mode in cfg.run_modes.items():
220
- needed = sum(cfg.agents[a].max_budget_usd for a in mode.agents if a in cfg.agents)
221
- assert needed <= mode.max_budget_usd, (
222
- f"mode '{name}' names agents that can spend ${needed:.2f} against a "
223
- f"${mode.max_budget_usd:.2f} cap; it will stop before the last agent runs"
224
- )
226
+ caps = [cfg.agents[a].max_budget_usd for a in mode.agents
227
+ if a in cfg.agents and cfg.agents[a].max_budget_usd is not None]
228
+ if caps and mode.max_budget_usd is not None:
229
+ assert sum(caps) <= mode.max_budget_usd, f"mode '{name}' cannot finish"
@@ -114,12 +114,12 @@ def test_one_agent_file_can_be_shadowed_without_forking_the_rest(tmp_path):
114
114
  override = tmp_path / "config" / "agents"
115
115
  override.mkdir(parents=True)
116
116
  shipped = (PACKAGED_CONFIG / "agents" / "mender.yaml").read_text()
117
- (override / "mender.yaml").write_text(shipped.replace("max_budget_usd: 5.0", "max_budget_usd: 9.0"))
117
+ (override / "mender.yaml").write_text(shipped.replace("max_turns: 80", "max_turns: 99"))
118
118
 
119
119
  cfg = load_config(search=(tmp_path / "config", *CONFIG_SEARCH))
120
- assert cfg.agents["MENDER"].max_budget_usd == 9.0, "the override did not win"
120
+ assert cfg.agents["MENDER"].max_turns == 99, "the override did not win"
121
121
  assert len(cfg.agents) == len(PACKAGED_AGENTS), "shadowing one agent must not drop the others"
122
- assert cfg.agents["CONDUIT"].max_budget_usd == 3.0, "an untouched agent changed"
122
+ assert cfg.agents["CONDUIT"].max_turns == 60, "an untouched agent changed"
123
123
 
124
124
 
125
125
  def test_system_yaml_is_taken_whole_from_the_first_layer(tmp_path):
@@ -115,10 +115,24 @@ def test_the_timeline_carries_the_facts_not_just_the_kinds(runner, run):
115
115
  assert "CORVID-1" in output
116
116
 
117
117
 
118
- def test_cost_accumulates_through_the_run(runner, run):
118
+ def test_cost_is_recorded_in_the_ledger_but_never_printed(runner, run):
119
+ """Cost is data, not display.
120
+
121
+ `qaas trace` used to carry a running-total column. The shipped config sets
122
+ no spend cap now -- a dollar figure is one vendor's price list, and this is
123
+ meant to run against local models too -- so printing money everywhere made
124
+ a Claude-specific assumption look like a property of the tool. The ledger
125
+ still records `cost_usd` on every `agent_finished`, so nothing is lost and
126
+ `--json` still carries it.
127
+ """
119
128
  output = _trace(runner, run)
120
- assert "$1.25" in output, "CONDUIT's cost"
121
- assert "$2.00" in output, "the running total after FORGE, not FORGE's $0.75"
129
+ assert "$" not in output, "a dollar figure reached the terminal"
130
+
131
+ entries = trace_mod.read_ledger(run)
132
+ finished = [e for e in entries if e.kind == "agent_finished"]
133
+ assert finished and any("cost_usd" in e.detail for e in finished), (
134
+ "cost stopped being recorded; it should only have stopped being printed"
135
+ )
122
136
 
123
137
 
124
138
  def test_consecutive_tool_calls_fold_into_one_row(run):
@@ -204,11 +218,11 @@ def test_json_output_survives_a_detail_longer_than_the_console(runner, tmp_path)
204
218
  # -- qaas show --------------------------------------------------------------
205
219
 
206
220
 
207
- def test_show_reports_cost_mode_duration_and_escalations(runner, run):
221
+ def test_show_reports_mode_duration_and_escalations(runner, run):
208
222
  result = runner.invoke(cli.app, ["show", run.run_id, "--root", str(run.root)])
209
223
  assert result.exit_code == 0, result.output
210
224
  assert "fix-cycle" in result.output
211
- assert "$2.00" in result.output, "cost, which show never printed"
225
+ assert "$" not in result.output, "no dollar figure should be printed"
212
226
  assert "duration" in result.output
213
227
  assert "escalations" in result.output and "CORVID-2 needs a human" in result.output
214
228
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes