agentforge-framework 0.2.1__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.1 → agentforge_framework-0.2.3}/PKG-INFO +23 -4
  2. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/README.md +22 -3
  3. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/pyproject.toml +1 -1
  4. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/__init__.py +1 -1
  5. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/decomposer.py +38 -5
  6. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/config.py +14 -1
  7. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/issues.py +32 -10
  8. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/project.py +8 -0
  9. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/repo.py +15 -0
  10. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/runtime.py +46 -11
  11. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/skills.py +78 -0
  12. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/base.py +35 -12
  13. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/claude.py +7 -4
  14. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/codex.py +8 -4
  15. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/PKG-INFO +23 -4
  16. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_cli.py +2 -3
  17. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_config.py +25 -0
  18. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_decompose.py +74 -1
  19. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_docs.py +23 -0
  20. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_execution_posture.py +2 -2
  21. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_interview.py +1 -1
  22. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_issues.py +38 -9
  23. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_plugins.py +2 -2
  24. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_project.py +6 -4
  25. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_providers.py +138 -7
  26. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_reviewer.py +3 -4
  27. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_runtime.py +66 -10
  28. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_skills.py +37 -0
  29. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_tester.py +1 -1
  30. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/LICENSE +0 -0
  31. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/setup.cfg +0 -0
  32. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/.claude-plugin/plugin.json +0 -0
  33. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/__init__.py +0 -0
  34. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/architect.py +0 -0
  35. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/implementer.py +0 -0
  36. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/orchestrator.py +0 -0
  37. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/reviewer.py +0 -0
  38. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/security.py +0 -0
  39. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/agents/tester.py +0 -0
  40. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/cli.py +0 -0
  41. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/__init__.py +0 -0
  42. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/__init__.py +0 -0
  43. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/base.py +0 -0
  44. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/python.py +0 -0
  45. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/sql.py +0 -0
  46. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/extractors/yaml.py +0 -0
  47. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/prompt.py +0 -0
  48. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/context/resolver.py +0 -0
  49. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/__init__.py +0 -0
  50. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/commands.py +0 -0
  51. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/contracts.py +0 -0
  52. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/gates.py +0 -0
  53. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/plan_format.py +0 -0
  54. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/process.py +0 -0
  55. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/registry.py +0 -0
  56. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/router.py +0 -0
  57. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/core/workflow.py +0 -0
  58. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/__init__.py +0 -0
  59. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/databricks/__init__.py +0 -0
  60. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/pyspark/__init__.py +0 -0
  61. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/python/__init__.py +0 -0
  62. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/plugins/sql/__init__.py +0 -0
  63. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/providers/__init__.py +0 -0
  64. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/MANIFEST.yaml +0 -0
  65. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/NOTICE +0 -0
  66. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +0 -0
  67. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +0 -0
  68. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/SKILL.md +0 -0
  69. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/domain-modeling/agents/openai.yaml +0 -0
  70. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/grill-with-docs/SKILL.md +0 -0
  71. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/grilling/SKILL.md +0 -0
  72. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/grilling/agents/openai.yaml +0 -0
  73. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-spec/SKILL.md +0 -0
  74. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-spec/agents/openai.yaml +0 -0
  75. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-tickets/SKILL.md +0 -0
  76. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/to-tickets/agents/openai.yaml +0 -0
  77. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/SKILL.md +0 -0
  78. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +0 -0
  79. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/_lang.py +0 -0
  80. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +0 -0
  81. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +0 -0
  82. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/calibrate_score.py +0 -0
  83. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/check_packs.py +0 -0
  84. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/check_suggestions.py +0 -0
  85. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/contribute.py +0 -0
  86. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/diff_check.py +0 -0
  87. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/extract_constraints.py +0 -0
  88. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/harvest_classify.py +0 -0
  89. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/harvest_samples.py +0 -0
  90. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/readability_metrics.py +0 -0
  91. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/refresh_status.py +0 -0
  92. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/silhouette_scan.py +0 -0
  93. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/structure_scan.py +0 -0
  94. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/suggest.py +0 -0
  95. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/validate_preservation.py +0 -0
  96. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/voice_card.py +0 -0
  97. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/voice_profile.py +0 -0
  98. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/voice_score.py +0 -0
  99. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/unslop/scripts/wiki_sync.py +0 -0
  100. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/skills/write-plainly/SKILL.md +0 -0
  101. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/workflows/bugfix.yaml +0 -0
  102. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/workflows/feature.yaml +0 -0
  103. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework/workflows/review.yaml +0 -0
  104. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/SOURCES.txt +0 -0
  105. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/dependency_links.txt +0 -0
  106. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/entry_points.txt +0 -0
  107. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/requires.txt +0 -0
  108. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/src/agentforge_framework.egg-info/top_level.txt +0 -0
  109. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_agents.py +0 -0
  110. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_architect.py +0 -0
  111. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_context.py +0 -0
  112. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_contracts.py +0 -0
  113. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_gates.py +0 -0
  114. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_plan_format.py +0 -0
  115. {agentforge_framework-0.2.1 → agentforge_framework-0.2.3}/tests/test_security.py +0 -0
  116. {agentforge_framework-0.2.1 → 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.1
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.1
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.1-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.1
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.1-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.1"
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.1"
3
+ __version__ = "0.2.3"
@@ -45,6 +45,8 @@ from ..core.contracts import (
45
45
  Task,
46
46
  )
47
47
  from ..core.plan_format import (
48
+ RESULT_CLOSE,
49
+ RESULT_OPEN,
48
50
  SLICES_CLOSE,
49
51
  SLICES_OPEN,
50
52
  SPEC_CLOSE,
@@ -105,8 +107,9 @@ not publish the spec, do not open an issue, and do not apply a label.
105
107
 
106
108
  ## Required output
107
109
 
108
- End your reply with this block and nothing after it. Markdown inside it, not \
109
- JSON:
110
+ End your reply with these two blocks, in this order, and nothing after them.
111
+
112
+ First the spec. Markdown inside it, not JSON:
110
113
 
111
114
  {spec_open}
112
115
  ## Problem Statement
@@ -128,10 +131,22 @@ JSON:
128
131
  ...
129
132
  {spec_close}
130
133
 
134
+ Then your own verdict:
135
+
136
+ {result_open}
137
+ ```json
138
+ {{"outcome": "completed", "summary": "one line on what the spec covers"}}
139
+ ```
140
+ {result_close}
141
+
131
142
  Cover the whole source. Anything in it that reaches no user story reaches no \
132
143
  Issue and does not get built. Scale the spec to what you were given: a \
133
144
  one-sentence Task earns a short spec, and padding one out invents scope nobody \
134
- asked for.\
145
+ asked for.
146
+
147
+ If the source is too thin to synthesize without inventing most of it, write the \
148
+ verdict block with `"outcome": "escalated"` and a summary naming what you need \
149
+ the human to supply. Omit the spec block in that case.\
135
150
  """
136
151
 
137
152
  SLICES = """\
@@ -186,7 +201,9 @@ publish anything, do not open an issue, and do not apply a label.
186
201
 
187
202
  ## Required output
188
203
 
189
- End your reply with this block and nothing after it:
204
+ End your reply with these two blocks, in this order, and nothing after them.
205
+
206
+ First the cut:
190
207
 
191
208
  {slices_open}
192
209
  ```json
@@ -202,7 +219,19 @@ End your reply with this block and nothing after it:
202
219
  ]
203
220
  }}
204
221
  ```
205
- {slices_close}\
222
+ {slices_close}
223
+
224
+ Then your own verdict:
225
+
226
+ {result_open}
227
+ ```json
228
+ {{"outcome": "completed", "summary": "one line on how you cut it and why"}}
229
+ ```
230
+ {result_close}
231
+
232
+ If the spec cannot be cut without guessing at something it left open, write the \
233
+ verdict block with `"outcome": "escalated"` and a summary naming what has to be \
234
+ settled first. Omit the slices block in that case.\
206
235
  """
207
236
 
208
237
  #: What one approved Slice looks like when it is handed to a planning pass. The
@@ -298,6 +327,8 @@ class Decomposer:
298
327
  cwd=cwd,
299
328
  spec_open=SPEC_OPEN,
300
329
  spec_close=SPEC_CLOSE,
330
+ result_open=RESULT_OPEN,
331
+ result_close=RESULT_CLOSE,
301
332
  ),
302
333
  cwd,
303
334
  )
@@ -311,6 +342,8 @@ class Decomposer:
311
342
  cap=MAX_SLICES,
312
343
  slices_open=SLICES_OPEN,
313
344
  slices_close=SLICES_CLOSE,
345
+ result_open=RESULT_OPEN,
346
+ result_close=RESULT_CLOSE,
314
347
  ),
315
348
  cwd,
316
349
  )
@@ -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
  "---",
@@ -14,6 +14,13 @@ one task. `COMPOSED` says what each expands to, because a composite has to
14
14
  survive both Capability Tiers — natively it fans out through the Skill tool, and
15
15
  as a Fragment there is no tool to fan out with, so the delivery path inlines what
16
16
  it names.
17
+
18
+ A skill's author can also forbid autonomous invocation outright, by marking
19
+ `disable-model-invocation` in the frontmatter. ADR-0005 chose delivery from one
20
+ input — what the Provider can do — and had none for what the skill permits, so a
21
+ marked skill offered natively is refused by the Skill tool and the Role escalates
22
+ instead of working. `fragment_only` reads the mark from the bundle as shipped, so
23
+ the rule survives a refresh that marks a skill we have never heard of.
17
24
  """
18
25
 
19
26
  from __future__ import annotations
@@ -22,8 +29,11 @@ import json
22
29
  import sys
23
30
  from collections.abc import Sequence
24
31
  from dataclasses import dataclass, field
32
+ from functools import lru_cache
25
33
  from pathlib import Path
26
34
 
35
+ import yaml
36
+
27
37
  from .process import CommandRunner, SubprocessRunner
28
38
 
29
39
  SKILLS_ROOT = Path(__file__).resolve().parent.parent / "skills"
@@ -69,6 +79,74 @@ def expand(names: Sequence[str]) -> tuple[str, ...]:
69
79
  return tuple(ordered)
70
80
 
71
81
 
82
+ #: The frontmatter key by which a skill's author says a model may not invoke it
83
+ #: autonomously — only a human typing the command may.
84
+ MODEL_INVOCATION_KEY = "disable-model-invocation"
85
+
86
+
87
+ def _frontmatter(text: str) -> dict:
88
+ """The YAML block a SKILL.md opens with, or nothing if it has none."""
89
+ if not text.startswith("---"):
90
+ return {}
91
+ parts = text.split("---", 2)
92
+ if len(parts) < 3:
93
+ return {}
94
+ data = yaml.safe_load(parts[1])
95
+ return data if isinstance(data, dict) else {}
96
+
97
+
98
+ @lru_cache(maxsize=1)
99
+ def fragment_only() -> frozenset[str]:
100
+ """Skills the bundle forbids a model to invoke, read from the bundle itself.
101
+
102
+ Hand-listing these would have exactly the problem vendoring exists to
103
+ prevent: a refresh that marks a third skill would be picked up by nobody,
104
+ and the failure arrives as an escalation from a planning pass rather than as
105
+ anything a reader would connect to a bundle bump.
106
+
107
+ Frontmatter that will not parse counts as marked. Fragment delivery costs
108
+ tokens; getting this wrong the other way costs the Run.
109
+ """
110
+ marked = set()
111
+ for path in sorted(SKILLS_ROOT.iterdir()):
112
+ source = path / "SKILL.md"
113
+ if not source.is_file():
114
+ continue
115
+ try:
116
+ data = _frontmatter(source.read_text(encoding="utf-8"))
117
+ except yaml.YAMLError:
118
+ marked.add(path.name)
119
+ continue
120
+ if data.get(MODEL_INVOCATION_KEY):
121
+ marked.add(path.name)
122
+ return frozenset(marked)
123
+
124
+
125
+ def forbids_model_invocation(name: str) -> bool:
126
+ """Whether a skill, or anything it composes, may not be model-invoked.
127
+
128
+ A composite is checked through its parts as well: natively it fans out
129
+ through the Skill tool, so a composite naming a marked skill would reach the
130
+ same refusal one indirection later.
131
+ """
132
+ marked = fragment_only()
133
+ return any(part in marked for part in expand((name,)))
134
+
135
+
136
+ def split_delivery(names: Sequence[str]) -> tuple[tuple[str, ...], tuple[str, ...]]:
137
+ """Partition declared skills into the natively deliverable and the rest.
138
+
139
+ This is consulted before the Capability Tier, not after it. A marked skill
140
+ has no native delivery at any tier, so the Fragment is not the degraded form
141
+ here — it is the only one.
142
+ """
143
+ native: list[str] = []
144
+ fragment: list[str] = []
145
+ for name in names:
146
+ (fragment if forbids_model_invocation(name) else native).append(name)
147
+ return tuple(native), tuple(fragment)
148
+
149
+
72
150
  class SkillNotFound(LookupError):
73
151
  """A skill was requested that is not in the vendored bundle."""
74
152