agentforge-framework 0.2.2__tar.gz → 0.2.4__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 (119) hide show
  1. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/PKG-INFO +46 -5
  2. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/README.md +45 -4
  3. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/pyproject.toml +1 -1
  4. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/__init__.py +1 -1
  5. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/__init__.py +21 -2
  6. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/architect.py +7 -2
  7. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/implementer.py +7 -2
  8. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/orchestrator.py +2 -0
  9. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/reviewer.py +2 -0
  10. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/security.py +19 -7
  11. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/tester.py +15 -2
  12. agentforge_framework-0.2.4/src/agentforge_framework/core/config.py +158 -0
  13. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/contracts.py +36 -0
  14. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/issues.py +32 -10
  15. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/project.py +8 -0
  16. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/repo.py +15 -0
  17. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/runtime.py +56 -12
  18. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/providers/base.py +27 -4
  19. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/providers/claude.py +55 -11
  20. agentforge_framework-0.2.4/src/agentforge_framework/providers/codex.py +196 -0
  21. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework.egg-info/PKG-INFO +46 -5
  22. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_agents.py +2 -2
  23. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_architect.py +1 -1
  24. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_cli.py +1 -1
  25. agentforge_framework-0.2.4/tests/test_config.py +162 -0
  26. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_docs.py +6 -0
  27. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_execution_posture.py +81 -19
  28. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_issues.py +38 -9
  29. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_project.py +6 -4
  30. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_providers.py +70 -2
  31. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_reviewer.py +1 -1
  32. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_runtime.py +147 -19
  33. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_security.py +23 -5
  34. agentforge_framework-0.2.2/src/agentforge_framework/core/config.py +0 -90
  35. agentforge_framework-0.2.2/src/agentforge_framework/providers/codex.py +0 -143
  36. agentforge_framework-0.2.2/tests/test_config.py +0 -77
  37. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/LICENSE +0 -0
  38. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/setup.cfg +0 -0
  39. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/.claude-plugin/plugin.json +0 -0
  40. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/agents/decomposer.py +0 -0
  41. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/cli.py +0 -0
  42. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/__init__.py +0 -0
  43. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/extractors/__init__.py +0 -0
  44. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/extractors/base.py +0 -0
  45. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/extractors/python.py +0 -0
  46. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/extractors/sql.py +0 -0
  47. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/extractors/yaml.py +0 -0
  48. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/prompt.py +0 -0
  49. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/context/resolver.py +0 -0
  50. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/__init__.py +0 -0
  51. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/commands.py +0 -0
  52. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/gates.py +0 -0
  53. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/plan_format.py +0 -0
  54. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/process.py +0 -0
  55. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/registry.py +0 -0
  56. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/router.py +0 -0
  57. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/skills.py +0 -0
  58. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/core/workflow.py +0 -0
  59. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/plugins/__init__.py +0 -0
  60. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/plugins/databricks/__init__.py +0 -0
  61. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/plugins/pyspark/__init__.py +0 -0
  62. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/plugins/python/__init__.py +0 -0
  63. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/plugins/sql/__init__.py +0 -0
  64. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/providers/__init__.py +0 -0
  65. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/MANIFEST.yaml +0 -0
  66. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/NOTICE +0 -0
  67. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +0 -0
  68. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +0 -0
  69. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/domain-modeling/SKILL.md +0 -0
  70. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/domain-modeling/agents/openai.yaml +0 -0
  71. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/grill-with-docs/SKILL.md +0 -0
  72. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/grilling/SKILL.md +0 -0
  73. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/grilling/agents/openai.yaml +0 -0
  74. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/to-spec/SKILL.md +0 -0
  75. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/to-spec/agents/openai.yaml +0 -0
  76. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/to-tickets/SKILL.md +0 -0
  77. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/to-tickets/agents/openai.yaml +0 -0
  78. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/SKILL.md +0 -0
  79. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +0 -0
  80. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/_lang.py +0 -0
  81. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +0 -0
  82. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +0 -0
  83. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/calibrate_score.py +0 -0
  84. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/check_packs.py +0 -0
  85. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/check_suggestions.py +0 -0
  86. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/contribute.py +0 -0
  87. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/diff_check.py +0 -0
  88. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/extract_constraints.py +0 -0
  89. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/harvest_classify.py +0 -0
  90. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/harvest_samples.py +0 -0
  91. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/readability_metrics.py +0 -0
  92. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/refresh_status.py +0 -0
  93. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/silhouette_scan.py +0 -0
  94. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/structure_scan.py +0 -0
  95. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/suggest.py +0 -0
  96. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/validate_preservation.py +0 -0
  97. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/voice_card.py +0 -0
  98. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/voice_profile.py +0 -0
  99. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/voice_score.py +0 -0
  100. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/unslop/scripts/wiki_sync.py +0 -0
  101. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/skills/write-plainly/SKILL.md +0 -0
  102. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/workflows/bugfix.yaml +0 -0
  103. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/workflows/feature.yaml +0 -0
  104. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework/workflows/review.yaml +0 -0
  105. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework.egg-info/SOURCES.txt +0 -0
  106. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework.egg-info/dependency_links.txt +0 -0
  107. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework.egg-info/entry_points.txt +0 -0
  108. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework.egg-info/requires.txt +0 -0
  109. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/src/agentforge_framework.egg-info/top_level.txt +0 -0
  110. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_context.py +0 -0
  111. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_contracts.py +0 -0
  112. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_decompose.py +0 -0
  113. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_gates.py +0 -0
  114. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_interview.py +0 -0
  115. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_plan_format.py +0 -0
  116. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_plugins.py +0 -0
  117. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_skills.py +0 -0
  118. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/tests/test_tester.py +0 -0
  119. {agentforge_framework-0.2.2 → agentforge_framework-0.2.4}/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.2
3
+ Version: 0.2.4
4
4
  Summary: A modular framework for coordinating specialized software agents.
5
5
  Author: Yash Mhatre
6
6
  License-Expression: Apache-2.0
@@ -99,7 +99,10 @@ 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
107
  now run. Plugins have landed, and with them the data-engineering ones: a
105
108
  repository whose files import `pyspark` has its Roles told to write DataFrame
@@ -145,7 +148,7 @@ without notice. See
145
148
  ```console
146
149
  $ pip install agentforge-framework
147
150
  $ agentforge --version
148
- agentforge 0.2.2
151
+ agentforge 0.2.4
149
152
  ```
150
153
 
151
154
  That is the whole of it. The two routes below the naming note are for a machine
@@ -169,7 +172,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
169
172
  and install the file:
170
173
 
171
174
  ```console
172
- $ pip install agentforge_framework-0.2.2-py3-none-any.whl
175
+ $ pip install agentforge_framework-0.2.4-py3-none-any.whl
173
176
  ```
174
177
 
175
178
  Publishing a release is a wizard rather than a checklist:
@@ -200,6 +203,14 @@ What each release contains is in [CHANGELOG.md](CHANGELOG.md).
200
203
 
201
204
  AgentForge never touches a model API and handles no credentials of its own. Whatever your coding-agent CLI is already authenticated with is what a Run costs.
202
205
 
206
+ ### Editors and IDEs
207
+
208
+ AgentForge is a terminal tool. It runs fine from an IDE's terminal, but the IDE is not a Provider and its assistant is not one either.
209
+
210
+ **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.
211
+
212
+ **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.
213
+
203
214
  ## Commands
204
215
 
205
216
  | Command | What it does |
@@ -274,14 +285,33 @@ defaults are Claude `native` and every other Provider `fragment`, and the
274
285
  providers:
275
286
  claude:
276
287
  capability_tier: native
288
+ # Optional. Overrides the adapter's own tier-to-model table; name only the
289
+ # tiers you disagree about, and the rest keep the shipped defaults.
290
+ models:
291
+ deep: claude-opus-5
277
292
  codex:
278
293
  capability_tier: fragment
279
294
 
295
+ # Optional. A Role's two declared axes: which class of model runs it, and how
296
+ # hard that model thinks. Both default to the table in ADR-0004.
297
+ roles:
298
+ security:
299
+ tier: deep
300
+ effort: max
301
+
280
302
  gates:
281
303
  tests:
282
304
  suite: pytest
283
305
  ```
284
306
 
307
+ There is no `roles.<name>.model` key, and naming one is an error rather than a
308
+ line that gets ignored. A Role declares a tier; the Provider maps that tier onto
309
+ a model. A model named per Role does not survive the next CLI release and does
310
+ not port to another Provider, which is what
311
+ [ADR-0004](docs/adr/0004-model-tiers-declared-not-named.md) exists to prevent --
312
+ override `providers.<name>.models.<tier>` if the mapping is what you disagree
313
+ with.
314
+
285
315
  There is no `plugins:` key. Which Plugins answer for a repository is decided per
286
316
  Run from the frozen plan's blast radius (ADR-0016), so a repository-level list
287
317
  would be inert and misleading -- init prints what it detected instead. The file
@@ -331,10 +361,21 @@ code rather than the verdict about the old code.
331
361
 
332
362
  Read [`CONTEXT.md`](CONTEXT.md) before writing anything, and [`docs/adr/`](docs/adr/) for the decisions that constrain it.
333
363
 
334
- ## Tests
364
+ ## Changing AgentForge
365
+
366
+ Clone it and install that clone editable. Never patch the installed copy.
335
367
 
336
368
  ```console
369
+ $ git clone https://github.com/yashmhatre/AgentForge.git
370
+ $ cd AgentForge
337
371
  $ pip install -e ".[dev]"
372
+ ```
373
+
374
+ Editing `site-packages/agentforge_framework/` appears to work and is the one change nothing will catch: the local install diverges from what ships, so the thing that works is not the thing anybody else gets, and the fix is lost the next time the package is upgraded. If a Run is failing badly enough that patching the install looks like the way forward, that is a bug worth filing — [#101](https://github.com/yashmhatre/AgentForge/issues/101) is what happened the last time somebody reached for it.
375
+
376
+ ## Tests
377
+
378
+ ```console
338
379
  $ pytest
339
380
  ```
340
381
 
@@ -81,7 +81,10 @@ 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
89
  now run. Plugins have landed, and with them the data-engineering ones: a
87
90
  repository whose files import `pyspark` has its Roles told to write DataFrame
@@ -127,7 +130,7 @@ without notice. See
127
130
  ```console
128
131
  $ pip install agentforge-framework
129
132
  $ agentforge --version
130
- agentforge 0.2.2
133
+ agentforge 0.2.4
131
134
  ```
132
135
 
133
136
  That is the whole of it. The two routes below the naming note are for a machine
@@ -151,7 +154,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
151
154
  and install the file:
152
155
 
153
156
  ```console
154
- $ pip install agentforge_framework-0.2.2-py3-none-any.whl
157
+ $ pip install agentforge_framework-0.2.4-py3-none-any.whl
155
158
  ```
156
159
 
157
160
  Publishing a release is a wizard rather than a checklist:
@@ -182,6 +185,14 @@ What each release contains is in [CHANGELOG.md](CHANGELOG.md).
182
185
 
183
186
  AgentForge never touches a model API and handles no credentials of its own. Whatever your coding-agent CLI is already authenticated with is what a Run costs.
184
187
 
188
+ ### Editors and IDEs
189
+
190
+ AgentForge is a terminal tool. It runs fine from an IDE's terminal, but the IDE is not a Provider and its assistant is not one either.
191
+
192
+ **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.
193
+
194
+ **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.
195
+
185
196
  ## Commands
186
197
 
187
198
  | Command | What it does |
@@ -256,14 +267,33 @@ defaults are Claude `native` and every other Provider `fragment`, and the
256
267
  providers:
257
268
  claude:
258
269
  capability_tier: native
270
+ # Optional. Overrides the adapter's own tier-to-model table; name only the
271
+ # tiers you disagree about, and the rest keep the shipped defaults.
272
+ models:
273
+ deep: claude-opus-5
259
274
  codex:
260
275
  capability_tier: fragment
261
276
 
277
+ # Optional. A Role's two declared axes: which class of model runs it, and how
278
+ # hard that model thinks. Both default to the table in ADR-0004.
279
+ roles:
280
+ security:
281
+ tier: deep
282
+ effort: max
283
+
262
284
  gates:
263
285
  tests:
264
286
  suite: pytest
265
287
  ```
266
288
 
289
+ There is no `roles.<name>.model` key, and naming one is an error rather than a
290
+ line that gets ignored. A Role declares a tier; the Provider maps that tier onto
291
+ a model. A model named per Role does not survive the next CLI release and does
292
+ not port to another Provider, which is what
293
+ [ADR-0004](docs/adr/0004-model-tiers-declared-not-named.md) exists to prevent --
294
+ override `providers.<name>.models.<tier>` if the mapping is what you disagree
295
+ with.
296
+
267
297
  There is no `plugins:` key. Which Plugins answer for a repository is decided per
268
298
  Run from the frozen plan's blast radius (ADR-0016), so a repository-level list
269
299
  would be inert and misleading -- init prints what it detected instead. The file
@@ -313,10 +343,21 @@ code rather than the verdict about the old code.
313
343
 
314
344
  Read [`CONTEXT.md`](CONTEXT.md) before writing anything, and [`docs/adr/`](docs/adr/) for the decisions that constrain it.
315
345
 
316
- ## Tests
346
+ ## Changing AgentForge
347
+
348
+ Clone it and install that clone editable. Never patch the installed copy.
317
349
 
318
350
  ```console
351
+ $ git clone https://github.com/yashmhatre/AgentForge.git
352
+ $ cd AgentForge
319
353
  $ pip install -e ".[dev]"
354
+ ```
355
+
356
+ Editing `site-packages/agentforge_framework/` appears to work and is the one change nothing will catch: the local install diverges from what ships, so the thing that works is not the thing anybody else gets, and the fix is lost the next time the package is upgraded. If a Run is failing badly enough that patching the install looks like the way forward, that is a bug worth filing — [#101](https://github.com/yashmhatre/AgentForge/issues/101) is what happened the last time somebody reached for it.
357
+
358
+ ## Tests
359
+
360
+ ```console
320
361
  $ pytest
321
362
  ```
322
363
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentforge-framework"
7
- version = "0.2.2"
7
+ version = "0.2.4"
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.2"
3
+ __version__ = "0.2.4"
@@ -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(
@@ -0,0 +1,158 @@
1
+ """Read project configuration without creating or changing it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shlex
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ from .contracts import Effort, ModelTier
13
+
14
+
15
+ class CapabilityTier(StrEnum):
16
+ """How a Provider receives a Role's declared Vendored Skills."""
17
+
18
+ NATIVE = "native"
19
+ FRAGMENT = "fragment"
20
+
21
+
22
+ DEFAULT_CAPABILITIES = {
23
+ "claude": CapabilityTier.NATIVE,
24
+ "codex": CapabilityTier.FRAGMENT,
25
+ }
26
+
27
+ #: What a `tests` Gate runs when the project declares nothing. `pytest` rather
28
+ #: than `python -m pytest`, so that an environment without it is a binary the
29
+ #: Command Runner cannot find — which is the difference between a suite that
30
+ #: could not be run and one that ran and failed, and the Gate reports them
31
+ #: differently.
32
+ DEFAULT_TEST_SUITE = ("pytest",)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Config:
37
+ """The read-only project configuration the Workflow runtime reads."""
38
+
39
+ provider_capabilities: dict[str, CapabilityTier] = field(
40
+ default_factory=lambda: dict(DEFAULT_CAPABILITIES)
41
+ )
42
+ #: The argument vector the test-suite Gate runs. An argument vector, not a
43
+ #: Command in the glossary's sense: nothing infers anything from it.
44
+ test_suite: tuple[str, ...] = DEFAULT_TEST_SUITE
45
+
46
+ #: Whether the Context Pack comment publishes the symbols and import graph
47
+ #: it resolved, or only their counts. Off by default: the Issue already
48
+ #: carries the pack's file paths in the frozen Plan, and nothing else on it
49
+ #: carries private symbol names. A tracker whose audience matches the
50
+ #: code's turns this on. See ADR-0024.
51
+ publish_pack_inventory: bool = False
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
+
67
+ def capability_for(self, provider: str) -> CapabilityTier:
68
+ return self.provider_capabilities.get(provider, CapabilityTier.FRAGMENT)
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
+
74
+
75
+ def load_config(root: Path | str) -> Config:
76
+ """Read ``.agentforge/config.yaml``, or return documented defaults."""
77
+ path = Path(root) / ".agentforge" / "config.yaml"
78
+ if not path.is_file():
79
+ return Config()
80
+
81
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
82
+ capabilities = dict(DEFAULT_CAPABILITIES)
83
+ models: dict[str, dict[ModelTier, str]] = {}
84
+ for name, provider in (data.get("providers") or {}).items():
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)
93
+ return Config(
94
+ provider_capabilities=capabilities,
95
+ test_suite=_test_suite(data),
96
+ publish_pack_inventory=bool(
97
+ (data.get("context") or {}).get("publish_inventory", False)
98
+ ),
99
+ provider_models=models,
100
+ role_tiers=tiers,
101
+ role_efforts=efforts,
102
+ )
103
+
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
+
129
+ def _test_suite(data: dict) -> tuple[str, ...]:
130
+ """`gates.tests.suite`: what the test-suite Gate runs in this repository.
131
+
132
+ A string is split the way a shell would; a list is taken as written, which
133
+ is how a path with a space in it gets named. Nothing here consults the tree
134
+ — a project that runs `npm test` says so, rather than being guessed at.
135
+
136
+ ADR-0007 defers a general execution allowlist to M5. This is the bounded
137
+ case that ADR names as the appropriate thing to persist: one suite, declared
138
+ by the project, run by AgentForge itself rather than by a Role.
139
+ """
140
+ value = ((data.get("gates") or {}).get("tests") or {}).get("suite")
141
+ if value is None:
142
+ return DEFAULT_TEST_SUITE
143
+
144
+ if isinstance(value, str):
145
+ parts = tuple(shlex.split(value))
146
+ elif isinstance(value, list):
147
+ parts = tuple(str(part) for part in value)
148
+ else:
149
+ raise TypeError(
150
+ f"`gates.tests.suite` must be a string or a list, not {type(value).__name__}"
151
+ )
152
+
153
+ if not parts:
154
+ raise ValueError("`gates.tests.suite` is empty; name the command that runs the suite")
155
+ return parts
156
+
157
+
158
+ __all__ = ["DEFAULT_TEST_SUITE", "CapabilityTier", "Config", "load_config"]
@@ -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: