agentforge-framework 0.2.3__tar.gz → 0.2.5__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 (120) hide show
  1. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/PKG-INFO +48 -12
  2. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/README.md +47 -11
  3. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/pyproject.toml +1 -1
  4. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/__init__.py +1 -1
  5. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/__init__.py +21 -2
  6. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/architect.py +7 -2
  7. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/implementer.py +7 -2
  8. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/orchestrator.py +2 -0
  9. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/reviewer.py +2 -0
  10. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/security.py +19 -7
  11. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/tester.py +15 -2
  12. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/cli.py +11 -6
  13. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/config.py +56 -1
  14. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/contracts.py +36 -0
  15. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/gates.py +35 -3
  16. agentforge_framework-0.2.5/src/agentforge_framework/core/project.py +445 -0
  17. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/repo.py +14 -0
  18. agentforge_framework-0.2.5/src/agentforge_framework/core/runlock.py +223 -0
  19. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/runtime.py +64 -1
  20. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/providers/base.py +27 -4
  21. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/providers/claude.py +55 -11
  22. agentforge_framework-0.2.5/src/agentforge_framework/providers/codex.py +196 -0
  23. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework.egg-info/PKG-INFO +48 -12
  24. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework.egg-info/SOURCES.txt +2 -0
  25. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_agents.py +2 -2
  26. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_architect.py +1 -1
  27. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_cli.py +26 -1
  28. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_config.py +60 -0
  29. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_docs.py +6 -0
  30. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_execution_posture.py +81 -19
  31. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_gates.py +74 -0
  32. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_project.py +183 -1
  33. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_providers.py +70 -2
  34. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_reviewer.py +1 -1
  35. agentforge_framework-0.2.5/tests/test_runlock.py +189 -0
  36. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_runtime.py +118 -21
  37. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_security.py +23 -5
  38. agentforge_framework-0.2.3/src/agentforge_framework/core/project.py +0 -270
  39. agentforge_framework-0.2.3/src/agentforge_framework/providers/codex.py +0 -143
  40. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/LICENSE +0 -0
  41. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/setup.cfg +0 -0
  42. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/.claude-plugin/plugin.json +0 -0
  43. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/agents/decomposer.py +0 -0
  44. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/__init__.py +0 -0
  45. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/extractors/__init__.py +0 -0
  46. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/extractors/base.py +0 -0
  47. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/extractors/python.py +0 -0
  48. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/extractors/sql.py +0 -0
  49. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/extractors/yaml.py +0 -0
  50. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/prompt.py +0 -0
  51. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/context/resolver.py +0 -0
  52. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/__init__.py +0 -0
  53. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/commands.py +0 -0
  54. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/issues.py +0 -0
  55. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/plan_format.py +0 -0
  56. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/process.py +0 -0
  57. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/registry.py +0 -0
  58. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/router.py +0 -0
  59. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/skills.py +0 -0
  60. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/core/workflow.py +0 -0
  61. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/plugins/__init__.py +0 -0
  62. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/plugins/databricks/__init__.py +0 -0
  63. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/plugins/pyspark/__init__.py +0 -0
  64. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/plugins/python/__init__.py +0 -0
  65. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/plugins/sql/__init__.py +0 -0
  66. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/providers/__init__.py +0 -0
  67. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/MANIFEST.yaml +0 -0
  68. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/NOTICE +0 -0
  69. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +0 -0
  70. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +0 -0
  71. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/domain-modeling/SKILL.md +0 -0
  72. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/domain-modeling/agents/openai.yaml +0 -0
  73. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/grill-with-docs/SKILL.md +0 -0
  74. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/grilling/SKILL.md +0 -0
  75. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/grilling/agents/openai.yaml +0 -0
  76. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/to-spec/SKILL.md +0 -0
  77. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/to-spec/agents/openai.yaml +0 -0
  78. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/to-tickets/SKILL.md +0 -0
  79. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/to-tickets/agents/openai.yaml +0 -0
  80. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/SKILL.md +0 -0
  81. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +0 -0
  82. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/_lang.py +0 -0
  83. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +0 -0
  84. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +0 -0
  85. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/calibrate_score.py +0 -0
  86. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/check_packs.py +0 -0
  87. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/check_suggestions.py +0 -0
  88. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/contribute.py +0 -0
  89. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/diff_check.py +0 -0
  90. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/extract_constraints.py +0 -0
  91. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/harvest_classify.py +0 -0
  92. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/harvest_samples.py +0 -0
  93. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/readability_metrics.py +0 -0
  94. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/refresh_status.py +0 -0
  95. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/silhouette_scan.py +0 -0
  96. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/structure_scan.py +0 -0
  97. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/suggest.py +0 -0
  98. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/validate_preservation.py +0 -0
  99. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/voice_card.py +0 -0
  100. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/voice_profile.py +0 -0
  101. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/voice_score.py +0 -0
  102. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/unslop/scripts/wiki_sync.py +0 -0
  103. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/skills/write-plainly/SKILL.md +0 -0
  104. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/workflows/bugfix.yaml +0 -0
  105. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/workflows/feature.yaml +0 -0
  106. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework/workflows/review.yaml +0 -0
  107. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework.egg-info/dependency_links.txt +0 -0
  108. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework.egg-info/entry_points.txt +0 -0
  109. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework.egg-info/requires.txt +0 -0
  110. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/src/agentforge_framework.egg-info/top_level.txt +0 -0
  111. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_context.py +0 -0
  112. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_contracts.py +0 -0
  113. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_decompose.py +0 -0
  114. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_interview.py +0 -0
  115. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_issues.py +0 -0
  116. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_plan_format.py +0 -0
  117. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_plugins.py +0 -0
  118. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_skills.py +0 -0
  119. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_tester.py +0 -0
  120. {agentforge_framework-0.2.3 → agentforge_framework-0.2.5}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentforge-framework
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: A modular framework for coordinating specialized software agents.
5
5
  Author: Yash Mhatre
6
6
  License-Expression: Apache-2.0
@@ -26,9 +26,9 @@ A human states a Task. The Orchestrator grills them on it, writes a spec, cuts t
26
26
 
27
27
  ## Status
28
28
 
29
- The Workflow runtime now runs multiple Roles in order. The default `feature`
30
- Workflow invokes the Implementer, the Tester, Security, and then the Reviewer,
31
- posting each Agent Result to the Issue before starting the next Step.
29
+ A Workflow runs its Roles in order. The default `feature` Workflow invokes the
30
+ Implementer, the Tester, Security, and then the Reviewer, posting each Agent
31
+ Result to the Issue before starting the next Step.
32
32
 
33
33
  ```console
34
34
  $ agentforge plan "add a retry to the loader"
@@ -99,15 +99,20 @@ ADR-0015.
99
99
 
100
100
  Without `--allow-commands`, the Implementer remains default-deny and the Tester
101
101
  reports that it could not run the suite; it never substitutes reading tests and
102
- claims completion. Security, the Reviewer, and the Architect need no such flag —
102
+ claims completion. That gate is held by the `claude` adapter. The `codex` CLI
103
+ cannot hold it — `codex exec` discards its approval flag — so a denied Run on
104
+ `codex` is refused before it starts rather than run under a guarantee nothing
105
+ enforces (ADR-0007, amended). Security, the Reviewer, and the Architect need no such flag —
103
106
  auditing, reviewing, and designing are reading. All six Roles `CONTEXT.md` names
104
- now run. Plugins have landed, and with them the data-engineering ones: a
107
+ run. Plugins carry the data-engineering conventions: a
105
108
  repository whose files import `pyspark` has its Roles told to write DataFrame
106
109
  expressions rather than RDDs, and a Databricks workspace gets Unity Catalog
107
110
  naming and the Delta MERGE idioms in the prompts that write code and the
108
111
  workspace's own posture in the one that audits it. A repository that matches
109
- neither hears neither. `agentforge init` is still to come. See
110
- [`docs/PLAN.md`](docs/PLAN.md).
112
+ neither hears neither. `agentforge init` inspects a repository and writes its
113
+ `.agentforge/config.yaml`; the file is optional, and
114
+ [Project configuration](#project-configuration) below says what it holds. What
115
+ is still to come is in [`docs/PLAN.md`](docs/PLAN.md).
111
116
 
112
117
  Before the first Role is invoked, AgentForge resolves a Context Pack from the
113
118
  frozen plan — the files it names, the symbols and imports inside them, the
@@ -145,7 +150,7 @@ without notice. See
145
150
  ```console
146
151
  $ pip install agentforge-framework
147
152
  $ agentforge --version
148
- agentforge 0.2.3
153
+ agentforge 0.2.5
149
154
  ```
150
155
 
151
156
  That is the whole of it. The two routes below the naming note are for a machine
@@ -169,7 +174,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
169
174
  and install the file:
170
175
 
171
176
  ```console
172
- $ pip install agentforge_framework-0.2.3-py3-none-any.whl
177
+ $ pip install agentforge_framework-0.2.5-py3-none-any.whl
173
178
  ```
174
179
 
175
180
  Publishing a release is a wizard rather than a checklist:
@@ -206,7 +211,7 @@ AgentForge is a terminal tool. It runs fine from an IDE's terminal, but the IDE
206
211
 
207
212
  **Antigravity IDE cannot be a Provider** ([#101](https://github.com/yashmhatre/AgentForge/issues/101)). Checked by running it, so nobody has to derive it again: `antigravity-ide chat` accepts a prompt and reads stdin, which is the right shape for the argument vector — but it is a window launcher. Piping a prompt to it returns exit 0 immediately with `Reading from stdin via: …\code-stdin-XXXX` on stdout and nothing else, ever; the answer goes to a GUI session. A Provider adapter has to read the Agent's result off stdout, so there is nothing for `parse_output` to parse. The same is true of any editor CLI of this shape.
208
213
 
209
- **Do not run another agent against a checkout while a Run is going.** A Run commits every change to a file git already tracks, however it got there ([ADR-0015](docs/adr/0015-a-run-commits-what-it-declared.md)) — so a second agent's half-finished edits are committed into the Run's branch and attributed to a Role. Files nothing in the Run claimed are listed in the pull request body ([ADR-0023](docs/adr/0023-a-commit-names-what-nothing-claimed.md)), which makes it visible at Sign-off but does not prevent it. Use a separate clone.
214
+ **Do not run another agent against a checkout while a Run is going.** A Run commits every change to a file git already tracks, however it got there ([ADR-0015](docs/adr/0015-a-run-commits-what-it-declared.md)) — so a second agent's half-finished edits are committed into the Run's branch and attributed to a Role. Files nothing in the Run claimed are listed in the pull request body ([ADR-0023](docs/adr/0023-a-commit-names-what-nothing-claimed.md)), which makes it visible at Sign-off but does not prevent it. Use a separate clone. A second `agentforge implement` is refused outright rather than merely disclosed — a Run holds its checkout for the duration ([ADR-0026](docs/adr/0026-a-run-holds-the-checkout-it-was-pointed-at.md)) — but nothing else consults that lock, so an IDE assistant is still yours to keep away.
210
215
 
211
216
  ## Commands
212
217
 
@@ -274,22 +279,53 @@ if the repository has no GitHub remote, because ADR-0002 makes that a
274
279
  precondition for every Run. Re-running never clobbers a config you have edited:
275
280
  it reports what differs and writes nothing, and `--force` replaces it.
276
281
 
282
+ A Python suite is written pinned to the interpreter that will run it —
283
+ `[".venv/bin/python", "-m", "pytest"]` rather than `pytest` — whenever the
284
+ repository has a virtualenv to pin it to, and init checks that the interpreter
285
+ can run pytest before writing the line. Bare `pytest` is whichever one PATH
286
+ answers with, which in a project with a venv is the one the project does not
287
+ use: the suite then fails on imports that are installed and reads as a broken
288
+ repository rather than a misconfigured one. The interpreter is named relative to
289
+ the repository root, so the file is still right on somebody else's clone, and
290
+ init looks up to two directories down for the project — a suite that lives in
291
+ `subproject/tests/` is found, and named.
292
+
277
293
  The file is not a precondition. Without one, the documented Provider capability
278
294
  defaults are Claude `native` and every other Provider `fragment`, and the
279
- `tests` Gate runs `pytest`.
295
+ `tests` Gate runs `pytest` — the case the pinning above exists to avoid, so a
296
+ repository with a venv is better off with a file.
280
297
 
281
298
  ```yaml
282
299
  providers:
283
300
  claude:
284
301
  capability_tier: native
302
+ # Optional. Overrides the adapter's own tier-to-model table; name only the
303
+ # tiers you disagree about, and the rest keep the shipped defaults.
304
+ models:
305
+ deep: claude-opus-5
285
306
  codex:
286
307
  capability_tier: fragment
287
308
 
309
+ # Optional. A Role's two declared axes: which class of model runs it, and how
310
+ # hard that model thinks. Both default to the table in ADR-0004.
311
+ roles:
312
+ security:
313
+ tier: deep
314
+ effort: max
315
+
288
316
  gates:
289
317
  tests:
290
318
  suite: pytest
291
319
  ```
292
320
 
321
+ There is no `roles.<name>.model` key, and naming one is an error rather than a
322
+ line that gets ignored. A Role declares a tier; the Provider maps that tier onto
323
+ a model. A model named per Role does not survive the next CLI release and does
324
+ not port to another Provider, which is what
325
+ [ADR-0004](docs/adr/0004-model-tiers-declared-not-named.md) exists to prevent --
326
+ override `providers.<name>.models.<tier>` if the mapping is what you disagree
327
+ with.
328
+
293
329
  There is no `plugins:` key. Which Plugins answer for a repository is decided per
294
330
  Run from the frozen plan's blast radius (ADR-0016), so a repository-level list
295
331
  would be inert and misleading -- init prints what it detected instead. The file
@@ -8,9 +8,9 @@ A human states a Task. The Orchestrator grills them on it, writes a spec, cuts t
8
8
 
9
9
  ## Status
10
10
 
11
- The Workflow runtime now runs multiple Roles in order. The default `feature`
12
- Workflow invokes the Implementer, the Tester, Security, and then the Reviewer,
13
- posting each Agent Result to the Issue before starting the next Step.
11
+ A Workflow runs its Roles in order. The default `feature` Workflow invokes the
12
+ Implementer, the Tester, Security, and then the Reviewer, posting each Agent
13
+ Result to the Issue before starting the next Step.
14
14
 
15
15
  ```console
16
16
  $ agentforge plan "add a retry to the loader"
@@ -81,15 +81,20 @@ ADR-0015.
81
81
 
82
82
  Without `--allow-commands`, the Implementer remains default-deny and the Tester
83
83
  reports that it could not run the suite; it never substitutes reading tests and
84
- claims completion. Security, the Reviewer, and the Architect need no such flag —
84
+ claims completion. That gate is held by the `claude` adapter. The `codex` CLI
85
+ cannot hold it — `codex exec` discards its approval flag — so a denied Run on
86
+ `codex` is refused before it starts rather than run under a guarantee nothing
87
+ enforces (ADR-0007, amended). Security, the Reviewer, and the Architect need no such flag —
85
88
  auditing, reviewing, and designing are reading. All six Roles `CONTEXT.md` names
86
- now run. Plugins have landed, and with them the data-engineering ones: a
89
+ run. Plugins carry the data-engineering conventions: a
87
90
  repository whose files import `pyspark` has its Roles told to write DataFrame
88
91
  expressions rather than RDDs, and a Databricks workspace gets Unity Catalog
89
92
  naming and the Delta MERGE idioms in the prompts that write code and the
90
93
  workspace's own posture in the one that audits it. A repository that matches
91
- neither hears neither. `agentforge init` is still to come. See
92
- [`docs/PLAN.md`](docs/PLAN.md).
94
+ neither hears neither. `agentforge init` inspects a repository and writes its
95
+ `.agentforge/config.yaml`; the file is optional, and
96
+ [Project configuration](#project-configuration) below says what it holds. What
97
+ is still to come is in [`docs/PLAN.md`](docs/PLAN.md).
93
98
 
94
99
  Before the first Role is invoked, AgentForge resolves a Context Pack from the
95
100
  frozen plan — the files it names, the symbols and imports inside them, the
@@ -127,7 +132,7 @@ without notice. See
127
132
  ```console
128
133
  $ pip install agentforge-framework
129
134
  $ agentforge --version
130
- agentforge 0.2.3
135
+ agentforge 0.2.5
131
136
  ```
132
137
 
133
138
  That is the whole of it. The two routes below the naming note are for a machine
@@ -151,7 +156,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
151
156
  and install the file:
152
157
 
153
158
  ```console
154
- $ pip install agentforge_framework-0.2.3-py3-none-any.whl
159
+ $ pip install agentforge_framework-0.2.5-py3-none-any.whl
155
160
  ```
156
161
 
157
162
  Publishing a release is a wizard rather than a checklist:
@@ -188,7 +193,7 @@ AgentForge is a terminal tool. It runs fine from an IDE's terminal, but the IDE
188
193
 
189
194
  **Antigravity IDE cannot be a Provider** ([#101](https://github.com/yashmhatre/AgentForge/issues/101)). Checked by running it, so nobody has to derive it again: `antigravity-ide chat` accepts a prompt and reads stdin, which is the right shape for the argument vector — but it is a window launcher. Piping a prompt to it returns exit 0 immediately with `Reading from stdin via: …\code-stdin-XXXX` on stdout and nothing else, ever; the answer goes to a GUI session. A Provider adapter has to read the Agent's result off stdout, so there is nothing for `parse_output` to parse. The same is true of any editor CLI of this shape.
190
195
 
191
- **Do not run another agent against a checkout while a Run is going.** A Run commits every change to a file git already tracks, however it got there ([ADR-0015](docs/adr/0015-a-run-commits-what-it-declared.md)) — so a second agent's half-finished edits are committed into the Run's branch and attributed to a Role. Files nothing in the Run claimed are listed in the pull request body ([ADR-0023](docs/adr/0023-a-commit-names-what-nothing-claimed.md)), which makes it visible at Sign-off but does not prevent it. Use a separate clone.
196
+ **Do not run another agent against a checkout while a Run is going.** A Run commits every change to a file git already tracks, however it got there ([ADR-0015](docs/adr/0015-a-run-commits-what-it-declared.md)) — so a second agent's half-finished edits are committed into the Run's branch and attributed to a Role. Files nothing in the Run claimed are listed in the pull request body ([ADR-0023](docs/adr/0023-a-commit-names-what-nothing-claimed.md)), which makes it visible at Sign-off but does not prevent it. Use a separate clone. A second `agentforge implement` is refused outright rather than merely disclosed — a Run holds its checkout for the duration ([ADR-0026](docs/adr/0026-a-run-holds-the-checkout-it-was-pointed-at.md)) — but nothing else consults that lock, so an IDE assistant is still yours to keep away.
192
197
 
193
198
  ## Commands
194
199
 
@@ -256,22 +261,53 @@ if the repository has no GitHub remote, because ADR-0002 makes that a
256
261
  precondition for every Run. Re-running never clobbers a config you have edited:
257
262
  it reports what differs and writes nothing, and `--force` replaces it.
258
263
 
264
+ A Python suite is written pinned to the interpreter that will run it —
265
+ `[".venv/bin/python", "-m", "pytest"]` rather than `pytest` — whenever the
266
+ repository has a virtualenv to pin it to, and init checks that the interpreter
267
+ can run pytest before writing the line. Bare `pytest` is whichever one PATH
268
+ answers with, which in a project with a venv is the one the project does not
269
+ use: the suite then fails on imports that are installed and reads as a broken
270
+ repository rather than a misconfigured one. The interpreter is named relative to
271
+ the repository root, so the file is still right on somebody else's clone, and
272
+ init looks up to two directories down for the project — a suite that lives in
273
+ `subproject/tests/` is found, and named.
274
+
259
275
  The file is not a precondition. Without one, the documented Provider capability
260
276
  defaults are Claude `native` and every other Provider `fragment`, and the
261
- `tests` Gate runs `pytest`.
277
+ `tests` Gate runs `pytest` — the case the pinning above exists to avoid, so a
278
+ repository with a venv is better off with a file.
262
279
 
263
280
  ```yaml
264
281
  providers:
265
282
  claude:
266
283
  capability_tier: native
284
+ # Optional. Overrides the adapter's own tier-to-model table; name only the
285
+ # tiers you disagree about, and the rest keep the shipped defaults.
286
+ models:
287
+ deep: claude-opus-5
267
288
  codex:
268
289
  capability_tier: fragment
269
290
 
291
+ # Optional. A Role's two declared axes: which class of model runs it, and how
292
+ # hard that model thinks. Both default to the table in ADR-0004.
293
+ roles:
294
+ security:
295
+ tier: deep
296
+ effort: max
297
+
270
298
  gates:
271
299
  tests:
272
300
  suite: pytest
273
301
  ```
274
302
 
303
+ There is no `roles.<name>.model` key, and naming one is an error rather than a
304
+ line that gets ignored. A Role declares a tier; the Provider maps that tier onto
305
+ a model. A model named per Role does not survive the next CLI release and does
306
+ not port to another Provider, which is what
307
+ [ADR-0004](docs/adr/0004-model-tiers-declared-not-named.md) exists to prevent --
308
+ override `providers.<name>.models.<tier>` if the mapping is what you disagree
309
+ with.
310
+
275
311
  There is no `plugins:` key. Which Plugins answer for a repository is decided per
276
312
  Run from the frozen plan's blast radius (ADR-0016), so a repository-level list
277
313
  would be inert and misleading -- init prints what it detected instead. The file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentforge-framework"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "A modular framework for coordinating specialized software agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """AgentForge — coordination of specialized software agents through reusable workflows."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.2.5"
@@ -12,7 +12,7 @@ would be the most expensive default in the project.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- from ..core.contracts import ModelTier, Role
15
+ from ..core.contracts import Effort, ModelTier, Role
16
16
  from .architect import ARCHITECT, Architect
17
17
  from .implementer import IMPLEMENTER, Implementer
18
18
  from .orchestrator import ORCHESTRATOR
@@ -53,12 +53,30 @@ RUNNERS: dict[str, type] = {
53
53
  KNOWN_TIERS: dict[str, ModelTier] = {
54
54
  "orchestrator": ModelTier.DEEP,
55
55
  "architect": ModelTier.DEEP,
56
- "security": ModelTier.DEEP,
57
56
  "reviewer": ModelTier.DEEP,
58
57
  "implementer": ModelTier.STANDARD,
58
+ "security": ModelTier.STANDARD,
59
59
  "tester": ModelTier.CHEAP,
60
60
  }
61
61
 
62
+ #: The second axis, keyed the same way. A tier says which model runs; this says
63
+ #: how hard it thinks, and the two are set independently — see ADR-0004's
64
+ #: 2026-09-03 amendment for why they were ever one thing.
65
+ #:
66
+ #: The Security row is the reason the axes split. It reads `standard` above and
67
+ #: `high` here, which is not a demotion: ADR-0004 bought it `deep` because a
68
+ #: missed finding is silent, and that argument was about reasoning depth. It
69
+ #: keeps the depth — above the Implementer whose work it audits — and stops
70
+ #: buying a frontier model to get it.
71
+ KNOWN_EFFORTS: dict[str, Effort] = {
72
+ "orchestrator": Effort.HIGH,
73
+ "architect": Effort.HIGH,
74
+ "reviewer": Effort.HIGH,
75
+ "security": Effort.HIGH,
76
+ "implementer": Effort.MEDIUM,
77
+ "tester": Effort.MEDIUM,
78
+ }
79
+
62
80
 
63
81
  class UnknownRole(LookupError):
64
82
  """A Roster names a Role this version of AgentForge cannot run."""
@@ -80,6 +98,7 @@ def resolve_role(name: str) -> Role:
80
98
  __all__ = [
81
99
  "ARCHITECT",
82
100
  "IMPLEMENTER",
101
+ "KNOWN_EFFORTS",
83
102
  "KNOWN_TIERS",
84
103
  "ORCHESTRATOR",
85
104
  "REVIEWER",
@@ -27,7 +27,7 @@ from __future__ import annotations
27
27
  from pathlib import Path
28
28
 
29
29
  from ..context.prompt import render_context_block
30
- from ..core.contracts import AgentResult, ContextPack, ModelTier, Plan, Role
30
+ from ..core.contracts import AgentResult, ContextPack, Effort, ModelTier, Plan, Role
31
31
  from ..core.plan_format import RESULT_CLOSE, RESULT_OPEN
32
32
  from .implementer import render_steps
33
33
 
@@ -90,7 +90,12 @@ step and the mismatch in `summary`.\
90
90
  """
91
91
 
92
92
  #: The Architect runs `deep`: a boundary in the wrong place outlives the Run.
93
- ARCHITECT = Role(name="architect", tier=ModelTier.DEEP, instructions=INSTRUCTIONS)
93
+ ARCHITECT = Role(
94
+ name="architect",
95
+ tier=ModelTier.DEEP,
96
+ effort=Effort.HIGH,
97
+ instructions=INSTRUCTIONS,
98
+ )
94
99
 
95
100
 
96
101
  def build_prompt(
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
  from pathlib import Path
20
20
 
21
21
  from ..context.prompt import render_context_block
22
- from ..core.contracts import AgentResult, ContextPack, ModelTier, Plan, Role
22
+ from ..core.contracts import AgentResult, ContextPack, Effort, ModelTier, Plan, Role
23
23
  from ..core.plan_format import RESULT_CLOSE, RESULT_OPEN
24
24
 
25
25
  INSTRUCTIONS = """\
@@ -77,7 +77,12 @@ actually contains. Change no files when you escalate.\
77
77
  """
78
78
 
79
79
  #: The Implementer runs at `standard`: it executes a plan it did not write.
80
- IMPLEMENTER = Role(name="implementer", tier=ModelTier.STANDARD, instructions=INSTRUCTIONS)
80
+ IMPLEMENTER = Role(
81
+ name="implementer",
82
+ tier=ModelTier.STANDARD,
83
+ effort=Effort.MEDIUM,
84
+ instructions=INSTRUCTIONS,
85
+ )
81
86
 
82
87
 
83
88
  def render_steps(plan: Plan) -> str:
@@ -38,6 +38,7 @@ from ..core.contracts import (
38
38
  DEFAULT_WORKFLOW,
39
39
  AgentResult,
40
40
  ContextPack,
41
+ Effort,
41
42
  ModelTier,
42
43
  Outcome,
43
44
  Plan,
@@ -239,6 +240,7 @@ repository itself uses them, and do not start a glossary during an interview.\
239
240
  ORCHESTRATOR = Role(
240
241
  name="orchestrator",
241
242
  tier=ModelTier.DEEP,
243
+ effort=Effort.HIGH,
242
244
  instructions=INSTRUCTIONS,
243
245
  #: What a planning pass works with: the project's vocabulary, the synthesis
244
246
  #: of a conversation into a spec, and the breakdown of that spec into work.
@@ -33,6 +33,7 @@ from ..context.prompt import render_context_block
33
33
  from ..core.contracts import (
34
34
  AgentResult,
35
35
  ContextPack,
36
+ Effort,
36
37
  ModelTier,
37
38
  Outcome,
38
39
  Plan,
@@ -158,6 +159,7 @@ End your reply with this block and nothing after it:
158
159
  REVIEWER = Role(
159
160
  name="reviewer",
160
161
  tier=ModelTier.DEEP,
162
+ effort=Effort.HIGH,
161
163
  instructions=INSTRUCTIONS,
162
164
  skills=WRITING_SKILLS,
163
165
  )
@@ -1,9 +1,15 @@
1
1
  """The Security Role: audits the change, and fixes nothing.
2
2
 
3
- It runs at `deep` per ADR-0004, which is the one tier decision in the project
4
- that is not about cost. A missed finding is silent nobody reviews the audit
5
- that did not happenso the Role that looks for what nobody asked about is the
6
- Role that cannot be run cheaply.
3
+ It runs at `standard` and thinks at `high` (ADR-0004, amended 2026-09-03). The
4
+ tier used to be `deep`, on the one argument in the table that was not about
5
+ cost: a missed finding is silent nobody reviews the audit that did not happen
6
+ — so the Role that looks for what nobody asked about cannot be run cheaply.
7
+
8
+ That argument survives; it was only ever about reasoning. When effort became a
9
+ Role's own axis, the depth it was really buying became purchasable on its own,
10
+ and Security kept it — `high`, above the Implementer whose work it reads —
11
+ while the tier came down. If audits start missing what a human then finds at
12
+ Sign-off, the tier is the half to move back.
7
13
 
8
14
  Its output is a list of Findings rather than prose, because the Gate downstream
9
15
  of it has to tell "audited, nothing found" from "did not audit", and a paragraph
@@ -16,7 +22,7 @@ from __future__ import annotations
16
22
  from pathlib import Path
17
23
 
18
24
  from ..context.prompt import render_context_block
19
- from ..core.contracts import AgentResult, ContextPack, ModelTier, Plan, Role
25
+ from ..core.contracts import AgentResult, ContextPack, Effort, ModelTier, Plan, Role
20
26
  from ..core.plan_format import RESULT_CLOSE, RESULT_OPEN
21
27
  from .implementer import render_steps
22
28
 
@@ -86,8 +92,14 @@ Use `"outcome": "escalated"` only when the Plan does not match the repository, \
86
92
  or when you could not audit the change at all. Say which in `summary`.\
87
93
  """
88
94
 
89
- #: Security runs `deep`: a finding nobody makes is a finding nobody reviews.
90
- SECURITY = Role(name="security", tier=ModelTier.DEEP, instructions=INSTRUCTIONS)
95
+ #: A finding nobody makes is a finding nobody reviews — bought as depth rather
96
+ #: than as model size. See the module docstring and ADR-0004.
97
+ SECURITY = Role(
98
+ name="security",
99
+ tier=ModelTier.STANDARD,
100
+ effort=Effort.HIGH,
101
+ instructions=INSTRUCTIONS,
102
+ )
91
103
 
92
104
 
93
105
  def build_prompt(
@@ -5,7 +5,15 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  from ..context.prompt import render_context_block
8
- from ..core.contracts import AgentResult, ContextPack, ModelTier, Outcome, Plan, Role
8
+ from ..core.contracts import (
9
+ AgentResult,
10
+ ContextPack,
11
+ Effort,
12
+ ModelTier,
13
+ Outcome,
14
+ Plan,
15
+ Role,
16
+ )
9
17
  from ..core.plan_format import RESULT_CLOSE, RESULT_OPEN
10
18
  from .implementer import render_steps
11
19
 
@@ -66,7 +74,12 @@ from verifying. Reading tests is not a substitute for running them.\
66
74
  #: recorded in ADR-0004 — reasoning about an edge case nobody wrote a test for is
67
75
  #: the part that gets worse here, and it is the part a human reads the findings
68
76
  #: for anyway.
69
- TESTER = Role(name="tester", tier=ModelTier.CHEAP, instructions=INSTRUCTIONS)
77
+ TESTER = Role(
78
+ name="tester",
79
+ tier=ModelTier.CHEAP,
80
+ effort=Effort.MEDIUM,
81
+ instructions=INSTRUCTIONS,
82
+ )
70
83
 
71
84
 
72
85
  def build_prompt(
@@ -512,7 +512,7 @@ def _run_init(args: argparse.Namespace, runner=None) -> int:
512
512
  from .core.config import load_config
513
513
  from .core.contracts import Plan
514
514
  from .core.process import SubprocessRunner
515
- from .core.project import config_path, detect, differences, render_config
515
+ from .core.project import config_path, detect, differences, render_config, verified
516
516
  from .core.registry import activate
517
517
  from .core.repo import PreconditionFailed, open_repository
518
518
  from .providers import DEFAULT_PROVIDER, PROVIDERS
@@ -531,11 +531,14 @@ def _run_init(args: argparse.Namespace, runner=None) -> int:
531
531
  return 2
532
532
 
533
533
  active = activate(Plan(summary=""), repo.root)
534
- context = detect(
535
- repo.root,
536
- provider,
537
- tracked=repo.tracked_files(),
538
- plugins=tuple(plugin.name for plugin in active.plugins),
534
+ context = verified(
535
+ detect(
536
+ repo.root,
537
+ provider,
538
+ tracked=repo.tracked_files(),
539
+ plugins=tuple(plugin.name for plugin in active.plugins),
540
+ ),
541
+ runner,
539
542
  )
540
543
 
541
544
  print(f"Repository: {repo.root}")
@@ -544,6 +547,8 @@ def _run_init(args: argparse.Namespace, runner=None) -> int:
544
547
  suite = " ".join(context.test_suite)
545
548
  where = context.suite_detected or "not detected, so this is the documented default"
546
549
  print(f" Suite: `{suite}` — {where}")
550
+ if context.suite_note:
551
+ print(f" {context.suite_note}")
547
552
  print(f" Plugins: {', '.join(context.plugins) or 'none by root marker'}")
548
553
  print(
549
554
  " printed, not written: which Plugins answer is decided per Run\n"
@@ -9,6 +9,8 @@ from pathlib import Path
9
9
 
10
10
  import yaml
11
11
 
12
+ from .contracts import Effort, ModelTier
13
+
12
14
 
13
15
  class CapabilityTier(StrEnum):
14
16
  """How a Provider receives a Role's declared Vendored Skills."""
@@ -48,9 +50,27 @@ class Config:
48
50
  #: code's turns this on. See ADR-0024.
49
51
  publish_pack_inventory: bool = False
50
52
 
53
+ #: Per-Provider overrides of the adapter's tier-to-model table, keyed by
54
+ #: provider name and then by tier. ADR-0004 promised this in its first
55
+ #: paragraph and nothing read it until 2026-09-03. A tier absent here keeps
56
+ #: the adapter's default, so a project pins the one tier it disagrees about
57
+ #: rather than restating all three.
58
+ provider_models: dict[str, dict[ModelTier, str]] = field(default_factory=dict)
59
+
60
+ #: Per-Role overrides of the two declared axes. The Role still never names a
61
+ #: model — it names a tier, and the Provider maps that. `roles.x.model` is
62
+ #: deliberately not a key: a model named per Role does not survive a release
63
+ #: and does not port across Providers, which is the whole of ADR-0004.
64
+ role_tiers: dict[str, ModelTier] = field(default_factory=dict)
65
+ role_efforts: dict[str, Effort] = field(default_factory=dict)
66
+
51
67
  def capability_for(self, provider: str) -> CapabilityTier:
52
68
  return self.provider_capabilities.get(provider, CapabilityTier.FRAGMENT)
53
69
 
70
+ def model_for(self, provider: str, tier: ModelTier) -> str | None:
71
+ """The configured model for a tier, or None to keep the adapter's own."""
72
+ return self.provider_models.get(provider, {}).get(tier)
73
+
54
74
 
55
75
  def load_config(root: Path | str) -> Config:
56
76
  """Read ``.agentforge/config.yaml``, or return documented defaults."""
@@ -60,17 +80,52 @@ def load_config(root: Path | str) -> Config:
60
80
 
61
81
  data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
62
82
  capabilities = dict(DEFAULT_CAPABILITIES)
83
+ models: dict[str, dict[ModelTier, str]] = {}
63
84
  for name, provider in (data.get("providers") or {}).items():
64
- capabilities[str(name)] = CapabilityTier(provider["capability_tier"])
85
+ if "capability_tier" in provider:
86
+ capabilities[str(name)] = CapabilityTier(provider["capability_tier"])
87
+ if provider.get("models"):
88
+ models[str(name)] = {
89
+ ModelTier(tier): str(slug) for tier, slug in provider["models"].items()
90
+ }
91
+
92
+ tiers, efforts = _role_overrides(data)
65
93
  return Config(
66
94
  provider_capabilities=capabilities,
67
95
  test_suite=_test_suite(data),
68
96
  publish_pack_inventory=bool(
69
97
  (data.get("context") or {}).get("publish_inventory", False)
70
98
  ),
99
+ provider_models=models,
100
+ role_tiers=tiers,
101
+ role_efforts=efforts,
71
102
  )
72
103
 
73
104
 
105
+ def _role_overrides(data: dict) -> tuple[dict[str, ModelTier], dict[str, Effort]]:
106
+ """`roles.<name>.tier` and `roles.<name>.effort`, the two declared axes.
107
+
108
+ A `model` key here is refused rather than ignored. Silently dropping it
109
+ would leave a project believing it had pinned a model per Role, which reads
110
+ as working right up until a Provider changes — and the whole reason
111
+ ADR-0004 gives a Role a tier is that the failure is otherwise invisible.
112
+ """
113
+ tiers: dict[str, ModelTier] = {}
114
+ efforts: dict[str, Effort] = {}
115
+ for name, role in (data.get("roles") or {}).items():
116
+ if "model" in role:
117
+ raise ValueError(
118
+ f"`roles.{name}.model` names a model per Role, which ADR-0004 "
119
+ f"does not allow; set `roles.{name}.tier` and override "
120
+ f"`providers.<name>.models.<tier>` if the mapping is wrong"
121
+ )
122
+ if "tier" in role:
123
+ tiers[str(name)] = ModelTier(role["tier"])
124
+ if "effort" in role:
125
+ efforts[str(name)] = Effort(role["effort"])
126
+ return tiers, efforts
127
+
128
+
74
129
  def _test_suite(data: dict) -> tuple[str, ...]:
75
130
  """`gates.tests.suite`: what the test-suite Gate runs in this repository.
76
131
 
@@ -45,6 +45,28 @@ class ModelTier(StrEnum):
45
45
  CHEAP = "cheap"
46
46
 
47
47
 
48
+ class Effort(StrEnum):
49
+ """How hard a Role thinks, named by intent and independent of its tier.
50
+
51
+ The second axis ADR-0004 originally folded into the first. A Model Tier
52
+ chooses which model runs; an Effort chooses how much reasoning it spends
53
+ getting there, and the two are not the same purchase -- the Security Role
54
+ is the case that proves it, auditing at `standard` and thinking at `high`.
55
+
56
+ The levels are the intersection of what the shipped adapters' CLIs accept:
57
+ `claude --effort` takes low through max, and every slug in a real codex
58
+ install takes the same five. `ultra` is offered by one codex model alone
59
+ and is deliberately absent -- a level a Role could declare and most
60
+ Providers could not honor is not an intent-named level.
61
+ """
62
+
63
+ LOW = "low"
64
+ MEDIUM = "medium"
65
+ HIGH = "high"
66
+ XHIGH = "xhigh"
67
+ MAX = "max"
68
+
69
+
48
70
  class Outcome(StrEnum):
49
71
  """How an Agent finished.
50
72
 
@@ -278,11 +300,25 @@ class Role:
278
300
  tier: ModelTier
279
301
  instructions: str = ""
280
302
  skills: tuple[str, ...] = ()
303
+ #: How much reasoning the Role spends, independent of which model runs it.
304
+ #: `medium` rather than a tier-derived value: a Role that has not thought
305
+ #: about the question gets the level both CLIs already default most models
306
+ #: to, and the ones that have thought about it say so.
307
+ effort: Effort = Effort.MEDIUM
281
308
 
282
309
  def at_tier(self, tier: ModelTier) -> Role:
283
310
  """The same Role with its tier overridden, per user request or config."""
284
311
  return replace(self, tier=tier)
285
312
 
313
+ def at_effort(self, effort: Effort) -> Role:
314
+ """The same Role with its effort overridden, per config.
315
+
316
+ Separate from `at_tier` because the axes move separately -- overriding
317
+ one has never implied anything about the other, which is the whole
318
+ reason there are two.
319
+ """
320
+ return replace(self, effort=effort)
321
+
286
322
 
287
323
  @dataclass(frozen=True)
288
324
  class Roster: