agentforge-framework 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 (116) hide show
  1. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/PKG-INFO +23 -4
  2. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/README.md +22 -3
  3. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/pyproject.toml +1 -1
  4. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/__init__.py +1 -1
  5. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/config.py +14 -1
  6. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/issues.py +32 -10
  7. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/project.py +8 -0
  8. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/repo.py +15 -0
  9. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/runtime.py +46 -11
  10. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/PKG-INFO +23 -4
  11. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_config.py +25 -0
  12. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_issues.py +38 -9
  13. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_project.py +6 -4
  14. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_runtime.py +56 -0
  15. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/LICENSE +0 -0
  16. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/setup.cfg +0 -0
  17. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/.claude-plugin/plugin.json +0 -0
  18. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/__init__.py +0 -0
  19. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/architect.py +0 -0
  20. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/decomposer.py +0 -0
  21. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/implementer.py +0 -0
  22. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/orchestrator.py +0 -0
  23. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/reviewer.py +0 -0
  24. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/security.py +0 -0
  25. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/tester.py +0 -0
  26. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/cli.py +0 -0
  27. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/__init__.py +0 -0
  28. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/__init__.py +0 -0
  29. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/base.py +0 -0
  30. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/python.py +0 -0
  31. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/sql.py +0 -0
  32. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/yaml.py +0 -0
  33. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/prompt.py +0 -0
  34. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/context/resolver.py +0 -0
  35. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/__init__.py +0 -0
  36. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/commands.py +0 -0
  37. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/contracts.py +0 -0
  38. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/gates.py +0 -0
  39. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/plan_format.py +0 -0
  40. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/process.py +0 -0
  41. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/registry.py +0 -0
  42. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/router.py +0 -0
  43. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/skills.py +0 -0
  44. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/core/workflow.py +0 -0
  45. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/__init__.py +0 -0
  46. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/databricks/__init__.py +0 -0
  47. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/pyspark/__init__.py +0 -0
  48. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/python/__init__.py +0 -0
  49. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/sql/__init__.py +0 -0
  50. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/__init__.py +0 -0
  51. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/base.py +0 -0
  52. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/claude.py +0 -0
  53. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/codex.py +0 -0
  54. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/MANIFEST.yaml +0 -0
  55. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/NOTICE +0 -0
  56. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +0 -0
  57. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +0 -0
  58. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/SKILL.md +0 -0
  59. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/agents/openai.yaml +0 -0
  60. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/grill-with-docs/SKILL.md +0 -0
  61. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/grilling/SKILL.md +0 -0
  62. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/grilling/agents/openai.yaml +0 -0
  63. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-spec/SKILL.md +0 -0
  64. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-spec/agents/openai.yaml +0 -0
  65. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-tickets/SKILL.md +0 -0
  66. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-tickets/agents/openai.yaml +0 -0
  67. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/SKILL.md +0 -0
  68. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +0 -0
  69. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/_lang.py +0 -0
  70. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +0 -0
  71. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +0 -0
  72. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/calibrate_score.py +0 -0
  73. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/check_packs.py +0 -0
  74. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/check_suggestions.py +0 -0
  75. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/contribute.py +0 -0
  76. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/diff_check.py +0 -0
  77. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/extract_constraints.py +0 -0
  78. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/harvest_classify.py +0 -0
  79. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/harvest_samples.py +0 -0
  80. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/readability_metrics.py +0 -0
  81. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/refresh_status.py +0 -0
  82. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/silhouette_scan.py +0 -0
  83. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/structure_scan.py +0 -0
  84. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/suggest.py +0 -0
  85. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/validate_preservation.py +0 -0
  86. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/voice_card.py +0 -0
  87. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/voice_profile.py +0 -0
  88. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/voice_score.py +0 -0
  89. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/wiki_sync.py +0 -0
  90. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/write-plainly/SKILL.md +0 -0
  91. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/workflows/bugfix.yaml +0 -0
  92. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/workflows/feature.yaml +0 -0
  93. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework/workflows/review.yaml +0 -0
  94. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/SOURCES.txt +0 -0
  95. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/dependency_links.txt +0 -0
  96. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/entry_points.txt +0 -0
  97. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/requires.txt +0 -0
  98. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/top_level.txt +0 -0
  99. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_agents.py +0 -0
  100. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_architect.py +0 -0
  101. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_cli.py +0 -0
  102. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_context.py +0 -0
  103. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_contracts.py +0 -0
  104. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_decompose.py +0 -0
  105. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_docs.py +0 -0
  106. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_execution_posture.py +0 -0
  107. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_gates.py +0 -0
  108. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_interview.py +0 -0
  109. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_plan_format.py +0 -0
  110. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_plugins.py +0 -0
  111. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_providers.py +0 -0
  112. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_reviewer.py +0 -0
  113. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_security.py +0 -0
  114. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_skills.py +0 -0
  115. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/tests/test_tester.py +0 -0
  116. {agentforge_framework-0.2.2 → agentforge_framework-0.2.3}/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.3
4
4
  Summary: A modular framework for coordinating specialized software agents.
5
5
  Author: Yash Mhatre
6
6
  License-Expression: Apache-2.0
@@ -145,7 +145,7 @@ without notice. See
145
145
  ```console
146
146
  $ pip install agentforge-framework
147
147
  $ agentforge --version
148
- agentforge 0.2.2
148
+ agentforge 0.2.3
149
149
  ```
150
150
 
151
151
  That is the whole of it. The two routes below the naming note are for a machine
@@ -169,7 +169,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
169
169
  and install the file:
170
170
 
171
171
  ```console
172
- $ pip install agentforge_framework-0.2.2-py3-none-any.whl
172
+ $ pip install agentforge_framework-0.2.3-py3-none-any.whl
173
173
  ```
174
174
 
175
175
  Publishing a release is a wizard rather than a checklist:
@@ -200,6 +200,14 @@ What each release contains is in [CHANGELOG.md](CHANGELOG.md).
200
200
 
201
201
  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
202
 
203
+ ### Editors and IDEs
204
+
205
+ 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.
206
+
207
+ **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
+
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.
210
+
203
211
  ## Commands
204
212
 
205
213
  | Command | What it does |
@@ -331,10 +339,21 @@ code rather than the verdict about the old code.
331
339
 
332
340
  Read [`CONTEXT.md`](CONTEXT.md) before writing anything, and [`docs/adr/`](docs/adr/) for the decisions that constrain it.
333
341
 
334
- ## Tests
342
+ ## Changing AgentForge
343
+
344
+ Clone it and install that clone editable. Never patch the installed copy.
335
345
 
336
346
  ```console
347
+ $ git clone https://github.com/yashmhatre/AgentForge.git
348
+ $ cd AgentForge
337
349
  $ pip install -e ".[dev]"
350
+ ```
351
+
352
+ 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.
353
+
354
+ ## Tests
355
+
356
+ ```console
338
357
  $ pytest
339
358
  ```
340
359
 
@@ -127,7 +127,7 @@ without notice. See
127
127
  ```console
128
128
  $ pip install agentforge-framework
129
129
  $ agentforge --version
130
- agentforge 0.2.2
130
+ agentforge 0.2.3
131
131
  ```
132
132
 
133
133
  That is the whole of it. The two routes below the naming note are for a machine
@@ -151,7 +151,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
151
151
  and install the file:
152
152
 
153
153
  ```console
154
- $ pip install agentforge_framework-0.2.2-py3-none-any.whl
154
+ $ pip install agentforge_framework-0.2.3-py3-none-any.whl
155
155
  ```
156
156
 
157
157
  Publishing a release is a wizard rather than a checklist:
@@ -182,6 +182,14 @@ What each release contains is in [CHANGELOG.md](CHANGELOG.md).
182
182
 
183
183
  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
184
 
185
+ ### Editors and IDEs
186
+
187
+ 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.
188
+
189
+ **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
+
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.
192
+
185
193
  ## Commands
186
194
 
187
195
  | Command | What it does |
@@ -313,10 +321,21 @@ code rather than the verdict about the old code.
313
321
 
314
322
  Read [`CONTEXT.md`](CONTEXT.md) before writing anything, and [`docs/adr/`](docs/adr/) for the decisions that constrain it.
315
323
 
316
- ## Tests
324
+ ## Changing AgentForge
325
+
326
+ Clone it and install that clone editable. Never patch the installed copy.
317
327
 
318
328
  ```console
329
+ $ git clone https://github.com/yashmhatre/AgentForge.git
330
+ $ cd AgentForge
319
331
  $ pip install -e ".[dev]"
332
+ ```
333
+
334
+ 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.
335
+
336
+ ## Tests
337
+
338
+ ```console
320
339
  $ pytest
321
340
  ```
322
341
 
@@ -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.3"
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.3"
@@ -41,6 +41,13 @@ class Config:
41
41
  #: Command in the glossary's sense: nothing infers anything from it.
42
42
  test_suite: tuple[str, ...] = DEFAULT_TEST_SUITE
43
43
 
44
+ #: Whether the Context Pack comment publishes the symbols and import graph
45
+ #: it resolved, or only their counts. Off by default: the Issue already
46
+ #: carries the pack's file paths in the frozen Plan, and nothing else on it
47
+ #: carries private symbol names. A tracker whose audience matches the
48
+ #: code's turns this on. See ADR-0024.
49
+ publish_pack_inventory: bool = False
50
+
44
51
  def capability_for(self, provider: str) -> CapabilityTier:
45
52
  return self.provider_capabilities.get(provider, CapabilityTier.FRAGMENT)
46
53
 
@@ -55,7 +62,13 @@ def load_config(root: Path | str) -> Config:
55
62
  capabilities = dict(DEFAULT_CAPABILITIES)
56
63
  for name, provider in (data.get("providers") or {}).items():
57
64
  capabilities[str(name)] = CapabilityTier(provider["capability_tier"])
58
- return Config(provider_capabilities=capabilities, test_suite=_test_suite(data))
65
+ return Config(
66
+ provider_capabilities=capabilities,
67
+ test_suite=_test_suite(data),
68
+ publish_pack_inventory=bool(
69
+ (data.get("context") or {}).get("publish_inventory", False)
70
+ ),
71
+ )
59
72
 
60
73
 
61
74
  def _test_suite(data: dict) -> tuple[str, ...]:
@@ -381,6 +381,7 @@ def render_context_comment(
381
381
  context: ContextPack,
382
382
  plugins: Sequence[tuple[str, str]] = (),
383
383
  skipped: Sequence[str] = (),
384
+ publish_inventory: bool = False,
384
385
  ) -> str:
385
386
  """The pack this Run's Agents were handed, recorded before the first one ran.
386
387
 
@@ -390,6 +391,14 @@ def render_context_comment(
390
391
  invocation (ADR-0010), so this is a record of a Run rather than a contract
391
392
  the next one reads back.
392
393
 
394
+ `publish_inventory` decides whether the resolved symbols and import graph
395
+ are named or only counted. Off by default: the Issue already carries this
396
+ pack's file paths in the frozen Plan, so the file list discloses nothing
397
+ new, while private symbol names and the graph between modules appear
398
+ nowhere else on the tracker (ADR-0024). The counts stay either way, because
399
+ a pack that resolved more or less than a reader expected is most of what
400
+ this comment is read for.
401
+
393
402
  `plugins` and `skipped` are the Plugins that answered for this Run and the
394
403
  ones that raised while being asked. Both are named for the same reason the
395
404
  pack is: a prompt that grew, or one that did not grow when a reader expected
@@ -418,16 +427,29 @@ def render_context_comment(
418
427
  lines += [f"**Files ({len(context.files)}):**", ""]
419
428
  lines += [f"- `{path}`" for path in context.files]
420
429
 
421
- for label, values in (("Symbols", context.symbols), ("Reaches for", context.references)):
422
- if values:
423
- lines += [
424
- "",
425
- f"<details><summary>{label} ({len(values)})</summary>",
426
- "",
427
- ", ".join(f"`{value}`" for value in values),
428
- "",
429
- "</details>",
430
- ]
430
+ inventory = (("Symbols", context.symbols), ("Reaches for", context.references))
431
+ if publish_inventory:
432
+ for label, values in inventory:
433
+ if values:
434
+ lines += [
435
+ "",
436
+ f"<details><summary>{label} ({len(values)})</summary>",
437
+ "",
438
+ ", ".join(f"`{value}`" for value in values),
439
+ "",
440
+ "</details>",
441
+ ]
442
+ elif any(values for _, values in inventory):
443
+ lines += [
444
+ "",
445
+ (
446
+ "Symbol and reference counts are in the heading; the names are not "
447
+ "published. The Issue already carries this pack's file paths in the "
448
+ "frozen Plan, and a reader diagnosing a Run reads them against the "
449
+ "repository. Set `context.publish_inventory: true` where the tracker's "
450
+ "audience is the code's audience (ADR-0024)."
451
+ ),
452
+ ]
431
453
 
432
454
  if context.conventions:
433
455
  lines += ["", "**Conventions:**", ""]
@@ -142,6 +142,14 @@ gates:
142
142
  # The argument vector the `tests` Gate runs, in this repository.
143
143
  # {because}
144
144
  suite: [{suite}]
145
+
146
+ context:
147
+ # Whether the Context Pack comment names the symbols and imports it resolved,
148
+ # or only counts them (ADR-0024). A Run posts that comment to the Issue, and
149
+ # a tracker can have a wider audience than the code — so the names are off
150
+ # unless this repository says otherwise. The file list is published either
151
+ # way: the frozen Plan on the Issue already carries it.
152
+ publish_inventory: false
145
153
  """
146
154
 
147
155
 
@@ -145,6 +145,21 @@ class Repository:
145
145
  self._git("push", "--set-upstream", "origin", branch)
146
146
 
147
147
 
148
+ def unclaimed(committed: Iterable[str], declared: Iterable[str]) -> tuple[str, ...]:
149
+ """Of the paths committed, the ones nothing in the Run said it would touch.
150
+
151
+ ADR-0015 commits every change to a tracked file however it arrived, and its
152
+ reasoning holds as long as the only writers are the Agents and the commands
153
+ they were allowed to run. A second agent editing the same checkout breaks
154
+ that — its half-finished work is committed into the Run's branch and
155
+ attributed to a Role, and nothing about the file on disk says otherwise
156
+ (#101). This does not change what is committed; it names what nothing
157
+ claimed, so the disclosure reaches the human at Sign-off.
158
+ """
159
+ named = {_normalize(path) for path in declared}
160
+ return tuple(path for path in committed if _normalize(path) not in named)
161
+
162
+
148
163
  def _normalize(path: str) -> str:
149
164
  r"""A declared path in the spelling `git status` uses.
150
165
 
@@ -24,7 +24,7 @@ from ..agents.decomposer import Approver, Decomposed, Decomposer, slice_task
24
24
  from ..agents.orchestrator import Exchange, Interviewer, Orchestrator, Planned
25
25
  from ..context.resolver import resolve_pack
26
26
  from ..providers import DEFAULT_PROVIDER, get_provider
27
- from .config import load_config
27
+ from .config import Config, load_config
28
28
  from .contracts import (
29
29
  AgentResult,
30
30
  ContextPack,
@@ -64,7 +64,13 @@ from .registry import (
64
64
  fragments_for,
65
65
  gates_for,
66
66
  )
67
- from .repo import PreconditionFailed, Repository, branch_for_issue, open_repository
67
+ from .repo import (
68
+ PreconditionFailed,
69
+ Repository,
70
+ branch_for_issue,
71
+ open_repository,
72
+ unclaimed,
73
+ )
68
74
  from .workflow import Workflow, WorkflowError, load_workflow
69
75
 
70
76
 
@@ -144,7 +150,9 @@ class Forge:
144
150
 
145
151
  # --- preconditions -----------------------------------------------------
146
152
 
147
- def _prepare(self, allow_commands: bool = False) -> tuple[Repository, GitHub, object]:
153
+ def _prepare(
154
+ self, allow_commands: bool = False
155
+ ) -> tuple[Repository, GitHub, object, Config]:
148
156
  """Every check that can fail for free, before anything is spent.
149
157
 
150
158
  Absent git, absent remote, absent `gh`, absent coding-agent CLI. A Run
@@ -157,11 +165,12 @@ class Forge:
157
165
  raise RunFailed(str(exc)) from exc
158
166
 
159
167
  github = GitHub(self.runner, repo.root)
168
+ config = load_config(repo.root)
160
169
  provider = get_provider(
161
170
  self.provider_name,
162
171
  self.runner,
163
172
  allow_commands=allow_commands,
164
- config=load_config(repo.root),
173
+ config=config,
165
174
  )
166
175
  try:
167
176
  github.preflight()
@@ -169,7 +178,7 @@ class Forge:
169
178
  except (IssueError, RuntimeError) as exc:
170
179
  raise RunFailed(str(exc)) from exc
171
180
 
172
- return repo, github, provider
181
+ return repo, github, provider, config
173
182
 
174
183
  # --- agentforge plan ---------------------------------------------------
175
184
 
@@ -208,7 +217,7 @@ class Forge:
208
217
  the caller supplies an approver that always says yes when the human
209
218
  asked for exactly that.
210
219
  """
211
- repo, github, provider = self._prepare()
220
+ repo, github, provider, _config = self._prepare()
212
221
 
213
222
  # Only when interviewing: every stage is told to change nothing, and two
214
223
  # extra `git status` calls on an unattended pass buy nothing.
@@ -368,7 +377,7 @@ class Forge:
368
377
  off already suppresses them, and a Run with neither cannot say which of
369
378
  the two moved the total. See ADR-0016 for the three conditions.
370
379
  """
371
- repo, github, provider = self._prepare(allow_commands=allow_commands)
380
+ repo, github, provider, config = self._prepare(allow_commands=allow_commands)
372
381
 
373
382
  if repo.is_dirty():
374
383
  raise RunFailed(
@@ -466,7 +475,10 @@ class Forge:
466
475
  github.post_comment(
467
476
  number,
468
477
  render_context_comment(
469
- state.context, contributions(activation), activation.skipped
478
+ state.context,
479
+ contributions(activation),
480
+ activation.skipped,
481
+ publish_inventory=config.publish_pack_inventory,
470
482
  ),
471
483
  )
472
484
  role = resolve_role(step.role)
@@ -535,14 +547,22 @@ class Forge:
535
547
  )
536
548
 
537
549
  changed = repo.changed_files()
550
+ declared = _declared_surface(state, results)
538
551
  committed = repo.commit_declared(
539
552
  f"{issue.title}\n\nImplements #{number} via AgentForge.",
540
- _declared_surface(state, results),
553
+ declared,
541
554
  )
542
555
  # Left in the working tree on purpose (ADR-0015), and named rather
543
556
  # than dropped: the human at Sign-off is the only one who can say
544
557
  # whether an undeclared file was an Agent's work or its suite's.
545
558
  left = tuple(path for path in changed if path not in committed)
559
+ # The other half of the same disclosure. A tracked file is committed
560
+ # however it changed (ADR-0015), which is right when the only writers
561
+ # are the Agents and their commands, and is how a second agent sharing
562
+ # the checkout gets its work committed and attributed to a Role (#101).
563
+ # Naming these is the difference between a data-loss bug and a line at
564
+ # Sign-off.
565
+ unclaimed_paths = unclaimed(committed, declared)
546
566
  base = github.default_branch()
547
567
  # An empty working tree is only a failure when the branch has nothing on
548
568
  # it either. Plenty of Runs legitimately write nothing here: an audit
@@ -573,7 +593,7 @@ class Forge:
573
593
  repo.push(branch)
574
594
  url = github.open_draft_pr(
575
595
  title=issue.title,
576
- body=_pr_body(number, state, results, committed, left),
596
+ body=_pr_body(number, state, results, committed, left, unclaimed_paths),
577
597
  head=branch,
578
598
  base=base,
579
599
  )
@@ -761,7 +781,7 @@ def _end(github: GitHub, issue: Issue, state: RunState) -> RunState:
761
781
  return state
762
782
 
763
783
 
764
- def _pr_body(number: int, state: RunState, results, committed, left=()) -> str:
784
+ def _pr_body(number: int, state: RunState, results, committed, left=(), unclaimed=()) -> str:
765
785
  lines = [
766
786
  f"Closes #{number}.",
767
787
  "",
@@ -791,6 +811,21 @@ def _pr_body(number: int, state: RunState, results, committed, left=()) -> str:
791
811
  "",
792
812
  ]
793
813
  lines += [f"- `{path}`" for path in left]
814
+ if unclaimed:
815
+ lines += [
816
+ "",
817
+ "## Committed, but no Agent claimed them",
818
+ "",
819
+ (
820
+ "These are in the diff. Git already tracked them, so AgentForge committed "
821
+ "them however they changed (ADR-0015) — but neither the Plan nor any Agent "
822
+ "Result named them, so nothing in this Run says they are its work. Read "
823
+ "them before you sign off. Another agent or a person editing this checkout "
824
+ "while the Run was going is the reason worth ruling out."
825
+ ),
826
+ "",
827
+ ]
828
+ lines += [f"- `{path}`" for path in unclaimed]
794
829
  lines += [
795
830
  "",
796
831
  "---",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentforge-framework
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A modular framework for coordinating specialized software agents.
5
5
  Author: Yash Mhatre
6
6
  License-Expression: Apache-2.0
@@ -145,7 +145,7 @@ without notice. See
145
145
  ```console
146
146
  $ pip install agentforge-framework
147
147
  $ agentforge --version
148
- agentforge 0.2.2
148
+ agentforge 0.2.3
149
149
  ```
150
150
 
151
151
  That is the whole of it. The two routes below the naming note are for a machine
@@ -169,7 +169,7 @@ from the [latest release](https://github.com/yashmhatre/AgentForge/releases/late
169
169
  and install the file:
170
170
 
171
171
  ```console
172
- $ pip install agentforge_framework-0.2.2-py3-none-any.whl
172
+ $ pip install agentforge_framework-0.2.3-py3-none-any.whl
173
173
  ```
174
174
 
175
175
  Publishing a release is a wizard rather than a checklist:
@@ -200,6 +200,14 @@ What each release contains is in [CHANGELOG.md](CHANGELOG.md).
200
200
 
201
201
  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
202
 
203
+ ### Editors and IDEs
204
+
205
+ 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.
206
+
207
+ **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
+
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.
210
+
203
211
  ## Commands
204
212
 
205
213
  | Command | What it does |
@@ -331,10 +339,21 @@ code rather than the verdict about the old code.
331
339
 
332
340
  Read [`CONTEXT.md`](CONTEXT.md) before writing anything, and [`docs/adr/`](docs/adr/) for the decisions that constrain it.
333
341
 
334
- ## Tests
342
+ ## Changing AgentForge
343
+
344
+ Clone it and install that clone editable. Never patch the installed copy.
335
345
 
336
346
  ```console
347
+ $ git clone https://github.com/yashmhatre/AgentForge.git
348
+ $ cd AgentForge
337
349
  $ pip install -e ".[dev]"
350
+ ```
351
+
352
+ 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.
353
+
354
+ ## Tests
355
+
356
+ ```console
338
357
  $ pytest
339
358
  ```
340
359
 
@@ -17,6 +17,31 @@ def test_missing_config_uses_the_documented_provider_capability_defaults(tmp_pat
17
17
  assert not (tmp_path / ".agentforge").exists(), "the loader is read-only"
18
18
 
19
19
 
20
+ def _config_file(tmp_path, text: str):
21
+ config_dir = tmp_path / ".agentforge"
22
+ config_dir.mkdir(exist_ok=True)
23
+ (config_dir / "config.yaml").write_text(text, encoding="utf-8")
24
+ return load_config(tmp_path)
25
+
26
+
27
+ def test_the_pack_inventory_is_not_published_unless_the_project_says_so():
28
+ """ADR-0024. The safe direction is the default because the disclosure is
29
+ the unrecoverable half: a comment on a public tracker is published."""
30
+ assert load_config("/nowhere").publish_pack_inventory is False
31
+
32
+
33
+ def test_a_project_whose_tracker_matches_its_code_publishes_the_inventory(tmp_path):
34
+ config = _config_file(tmp_path, "context:\n publish_inventory: true\n")
35
+
36
+ assert config.publish_pack_inventory is True
37
+
38
+
39
+ def test_a_project_can_say_no_as_explicitly_as_it_can_say_yes(tmp_path):
40
+ config = _config_file(tmp_path, "context:\n publish_inventory: false\n")
41
+
42
+ assert config.publish_pack_inventory is False
43
+
44
+
20
45
  def test_capability_tiers_are_read_from_the_shared_config_file(tmp_path):
21
46
  config_dir = tmp_path / ".agentforge"
22
47
  config_dir.mkdir()
@@ -721,23 +721,52 @@ def test_a_run_where_nobody_reported_anything_says_nobody_reported_anything():
721
721
  # --- the pack, recorded where a Run is diagnosed ---------------------------
722
722
 
723
723
 
724
+ A_PACK = ContextPack(
725
+ files=("src/loader.py",),
726
+ symbols=("src/loader.py::fetch",),
727
+ references=("requests",),
728
+ conventions=("no new dependencies",),
729
+ )
730
+
731
+
724
732
  def test_the_run_log_records_the_pack_the_agents_were_shown():
725
733
  """A Run that went wrong is diagnosed against what its Agents could see."""
726
- comment = render_context_comment(
727
- ContextPack(
728
- files=("src/loader.py",),
729
- symbols=("src/loader.py::fetch",),
730
- references=("requests",),
731
- conventions=("no new dependencies",),
732
- )
733
- )
734
+ comment = render_context_comment(A_PACK)
734
735
 
735
736
  assert "### Context Pack" in comment
736
737
  assert "`src/loader.py`" in comment
737
- assert "`src/loader.py::fetch`" in comment
738
738
  assert "no new dependencies" in comment
739
739
 
740
740
 
741
+ def test_the_counts_survive_the_names_not_being_published():
742
+ """What the comment is read for. A pack that resolved more or less than the
743
+ reader expected is visible without naming anything (ADR-0024)."""
744
+ comment = render_context_comment(A_PACK)
745
+
746
+ assert "1 file, 1 symbol, 1 reference" in comment
747
+ assert "src/loader.py::fetch" not in comment
748
+ assert "requests" not in comment
749
+ assert "publish_inventory" in comment, "a reader who wants the names is told how"
750
+
751
+
752
+ def test_the_inventory_is_published_when_the_repository_asks_for_it():
753
+ """A tracker whose audience is the code's audience loses nothing by it."""
754
+ comment = render_context_comment(A_PACK, publish_inventory=True)
755
+
756
+ assert "`src/loader.py::fetch`" in comment
757
+ assert "`requests`" in comment
758
+ assert "<details><summary>Symbols (1)</summary>" in comment
759
+
760
+
761
+ def test_a_pack_with_no_symbols_explains_nothing_it_withheld():
762
+ """The note is a disclosure about withheld names. A pack that resolved none
763
+ has nothing to withhold, and saying so would be noise."""
764
+ comment = render_context_comment(ContextPack(files=("src/loader.py",)))
765
+
766
+ assert "publish_inventory" not in comment
767
+ assert "`src/loader.py`" in comment
768
+
769
+
741
770
  def test_a_run_with_no_pack_says_that_it_is_the_control():
742
771
  """The measurement in story twenty needs two Runs and a way to tell which
743
772
  of them was which."""
@@ -117,6 +117,7 @@ def test_the_written_file_round_trips_through_load_config(tmp_path):
117
117
  config = load_config(tmp_path)
118
118
  assert config.test_suite == context.test_suite
119
119
  assert config.capability_for("codex") is context.capability_tier
120
+ assert config.publish_pack_inventory is False
120
121
 
121
122
 
122
123
  def test_an_argument_with_a_dash_in_it_survives_the_round_trip(tmp_path):
@@ -138,12 +139,13 @@ def test_the_file_says_which_values_were_detected_and_which_were_defaulted():
138
139
 
139
140
 
140
141
  def test_the_file_carries_no_key_nothing_reads():
141
- """`docs/PLAN.md` promised a config owning plugin activation; `load_config`
142
- reads two keys. A key that has no effect is a lie told to whoever edits it,
143
- so activation is printed and the file says why (ADR-0020)."""
142
+ """`docs/PLAN.md` promised a config owning plugin activation, and the file
143
+ holds only what `load_config` reads. A key that has no effect is a lie told
144
+ to whoever edits it, so activation is printed and the file says why
145
+ (ADR-0020). Every key here arrived with its reader."""
144
146
  written = render_config(a_context(plugins=("sql", "databricks")))
145
147
 
146
- assert set(yaml.safe_load(written)) == {"providers", "gates"}
148
+ assert set(yaml.safe_load(written)) == {"providers", "gates", "context"}
147
149
  assert "databricks" not in written
148
150
  # The absence is explained where somebody looking for the key will look.
149
151
  assert "no `plugins:` key here to edit" in written
@@ -1903,6 +1903,62 @@ def test_the_pull_request_lists_only_what_was_committed():
1903
1903
  assert "loader.cpython-311.pyc" not in body.split("## Left uncommitted")[0]
1904
1904
 
1905
1905
 
1906
+ #: A second agent editing the same checkout while the Run is going. The Run's
1907
+ #: own file is modified as expected; `src/telemetry.py` is somebody else's
1908
+ #: half-finished work, and git tracks it, so ADR-0015 commits it either way.
1909
+ CONCURRENT_EDIT = " M src/loader.py\n M src/telemetry.py\n"
1910
+
1911
+
1912
+ def test_a_committed_file_no_agent_claimed_is_named_in_the_pull_request():
1913
+ """The concurrency hazard in #101. Antigravity's agent and a Run share one
1914
+ checkout; ADR-0015 commits every change to a tracked file however it
1915
+ arrived, so the other agent's work lands in the branch attributed to a Role.
1916
+ Nothing about the file on disk says otherwise, so the Run says it instead."""
1917
+ runner = a_runner()
1918
+ runner.script("git", "status", "--porcelain", stdout=["", CONCURRENT_EDIT])
1919
+ runner.script("claude", stdout=agent_says("completed", "retry added"))
1920
+
1921
+ forge(runner).implement(12, allow_commands=True)
1922
+
1923
+ assert "src/telemetry.py" in staged(runner), "ADR-0015 still commits it"
1924
+ body = runner.argument_after("--body", "gh", "pr", "create")
1925
+ unclaimed_section = body.split("## Committed, but no Agent claimed them")[1]
1926
+ assert "- `src/telemetry.py`" in unclaimed_section
1927
+ assert "- `src/loader.py`" not in unclaimed_section, (
1928
+ "the Role declared this one; naming it would bury the one that matters"
1929
+ )
1930
+
1931
+
1932
+ def test_a_run_whose_every_file_was_declared_says_nothing_about_unclaimed_ones():
1933
+ """The section is a disclosure, not a fixture of the body. An ordinary Run
1934
+ that names what it changed reads exactly as it did before."""
1935
+ runner = a_runner()
1936
+ runner.script("git", "status", "--porcelain", stdout=["", " M src/loader.py\n"])
1937
+ runner.script("claude", stdout=agent_says("completed", "retry added"))
1938
+
1939
+ forge(runner).implement(12, allow_commands=True)
1940
+
1941
+ body = runner.argument_after("--body", "gh", "pr", "create")
1942
+ assert "no Agent claimed" not in body
1943
+
1944
+
1945
+ def test_a_role_that_spelled_its_path_the_windows_way_still_claimed_the_file():
1946
+ """A Role reports `src\\telemetry.py` and git answers `src/telemetry.py`.
1947
+ Comparing those literally would accuse an Agent of somebody else's edit on
1948
+ every Windows Run, which is the platform this hazard was found on."""
1949
+ runner = a_runner()
1950
+ runner.script("git", "status", "--porcelain", stdout=["", CONCURRENT_EDIT])
1951
+ runner.script(
1952
+ "claude",
1953
+ stdout=agent_says("completed", "retry added", files=("src\\telemetry.py",)),
1954
+ )
1955
+
1956
+ forge(runner).implement(12, allow_commands=True)
1957
+
1958
+ body = runner.argument_after("--body", "gh", "pr", "create")
1959
+ assert "no Agent claimed" not in body
1960
+
1961
+
1906
1962
  def test_what_was_left_behind_is_named_rather_than_dropped():
1907
1963
  """A build artifact is the usual reason and an Agent writing outside its
1908
1964
  Step is the one worth reading, and only a human can tell them apart."""