agentic-dev-loop 0.9.2__tar.gz → 0.10.2__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 (128) hide show
  1. {agentic_dev_loop-0.9.2/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.10.2}/PKG-INFO +81 -32
  2. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/README.md +80 -31
  3. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/pyproject.toml +3 -1
  4. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/config.py +58 -5
  5. agentic_dev_loop-0.10.2/src/agentic_dev_loop/core/profiles/__init__.py +178 -0
  6. agentic_dev_loop-0.10.2/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +22 -0
  7. agentic_dev_loop-0.10.2/src/agentic_dev_loop/core/profiles/data/python.yaml +8 -0
  8. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/cli.py +6 -0
  9. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/graph.py +14 -0
  10. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/hostboundary.py +3 -0
  11. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/llm.py +101 -0
  12. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/__init__.py +6 -0
  13. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/checks.py +272 -0
  14. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/doctor.py +84 -0
  15. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/init.py +432 -0
  16. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +85 -0
  17. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +57 -0
  18. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/recipes.py +77 -0
  19. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +133 -0
  20. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/env.example +11 -0
  21. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +181 -0
  22. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +53 -0
  23. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +97 -0
  24. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates.py +62 -0
  25. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/stages.py +14 -6
  26. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2/src/agentic_dev_loop.egg-info}/PKG-INFO +81 -32
  27. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/SOURCES.txt +24 -0
  28. agentic_dev_loop-0.10.2/tests/test_055_cli.py +51 -0
  29. agentic_dev_loop-0.10.2/tests/test_055_doctor.py +163 -0
  30. agentic_dev_loop-0.10.2/tests/test_055_init.py +135 -0
  31. agentic_dev_loop-0.10.2/tests/test_055_profile_loading.py +73 -0
  32. agentic_dev_loop-0.10.2/tests/test_055_profiles.py +156 -0
  33. agentic_dev_loop-0.10.2/tests/test_055_recipes.py +84 -0
  34. agentic_dev_loop-0.10.2/tests/test_055_schema_lock.py +42 -0
  35. agentic_dev_loop-0.10.2/tests/test_055_templates.py +80 -0
  36. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_config.py +88 -0
  37. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_graph.py +66 -1
  38. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_hostboundary.py +10 -0
  39. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_llm.py +89 -36
  40. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_module_contract.py +20 -1
  41. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_publish.py +38 -0
  42. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_sdk_compat.py +6 -5
  43. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_stages.py +80 -0
  44. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_trigger_script.py +84 -32
  45. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_viz.py +1 -1
  46. agentic_dev_loop-0.9.2/src/agentic_dev_loop/engine/llm.py +0 -67
  47. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/LICENSE +0 -0
  48. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/setup.cfg +0 -0
  49. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/__init__.py +0 -0
  50. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/__init__.py +0 -0
  51. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/paths.py +0 -0
  52. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/results.py +0 -0
  53. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/verdict.py +0 -0
  54. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/__init__.py +0 -0
  55. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/actions.py +0 -0
  56. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  57. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  58. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/agents.py +0 -0
  59. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  60. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  61. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  62. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  63. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/containers.py +0 -0
  64. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/deps.py +0 -0
  65. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  66. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  67. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  68. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  69. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  70. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  71. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/isolation.py +0 -0
  72. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/loaders.py +0 -0
  73. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/mounts.py +0 -0
  74. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/outputs.py +0 -0
  75. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/presets.py +0 -0
  76. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/proc.py +0 -0
  77. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/registry.py +0 -0
  78. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  79. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  80. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/secrets.py +0 -0
  81. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  82. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  83. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/py.typed +0 -0
  84. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/viz/__init__.py +0 -0
  85. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/viz/__main__.py +0 -0
  86. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/viz/server.py +0 -0
  87. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  88. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  89. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  90. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  91. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_029_merge_on_conflict.py +0 -0
  92. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_033_brief_step.py +0 -0
  93. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_035_finalize_nudge.py +0 -0
  94. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  95. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_041_brief_coverage.py +0 -0
  96. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_046_brief_prompt.py +0 -0
  97. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_actions.py +0 -0
  98. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_agent_error_guide.py +0 -0
  99. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_agents.py +0 -0
  100. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_aidlc.py +0 -0
  101. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_cli.py +0 -0
  102. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_containers.py +0 -0
  103. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_deps.py +0 -0
  104. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_dry_run.py +0 -0
  105. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_e2e_fixtures.py +0 -0
  106. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_egress_proxy.py +0 -0
  107. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_hostboundary_integration.py +0 -0
  108. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_isolation.py +0 -0
  109. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_isolation_integration.py +0 -0
  110. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_isolation_pbt.py +0 -0
  111. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_loaders.py +0 -0
  112. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_outputs.py +0 -0
  113. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_outputs_pbt.py +0 -0
  114. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_proc.py +0 -0
  115. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_qa_prompt.py +0 -0
  116. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_registry.py +0 -0
  117. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_repo_ascii.py +0 -0
  118. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_results.py +0 -0
  119. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_secrets.py +0 -0
  120. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_secrets_integration.py +0 -0
  121. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_secrets_pipeline.py +0 -0
  122. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_source_resolution.py +0 -0
  123. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_step_workspace.py +0 -0
  124. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_sync_back.py +0 -0
  125. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_unit5_config.py +0 -0
  126. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_unit5_pbt.py +0 -0
  127. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_unit5_pure.py +0 -0
  128. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.2}/tests/test_verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.9.2
3
+ Version: 0.10.2
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -202,6 +202,17 @@ llm:
202
202
  api_key_env: OPENAI_API_KEY
203
203
  # reasoning_effort: medium # optional: low | medium | high | xhigh | none
204
204
 
205
+ # Named LLM profiles (054): a step names one with `llm: <name>` to move the
206
+ # whole profile (model, endpoint, key, effort default) together - no partial
207
+ # merges. The `llm:` block above remains the default. Profile keys are
208
+ # host-side: sandbox access still goes through `secrets:`, never a profile key.
209
+ # llms:
210
+ # review:
211
+ # model: openai/deepseek-v4-pro
212
+ # base_url: https://api.deepseek.com
213
+ # api_key_env: REVIEW_API_KEY
214
+ # reasoning_effort: xhigh
215
+
205
216
  max_attempts: 10 # CLI --max-attempts overrides
206
217
  agents_dir: sdk_agents # your <name>.agent.md files
207
218
 
@@ -231,7 +242,8 @@ steps:
231
242
  agent: review
232
243
  artifact: review-findings.md
233
244
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
234
- # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
245
+ # reasoning_effort: xhigh # per-step override of the effective profile's effort
246
+ # llm: review # (054) select a named `llms:` profile for this step
235
247
  on_pass: qa
236
248
  on_fail: { goto: dev, retry: true }
237
249
 
@@ -257,11 +269,13 @@ steps:
257
269
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
258
270
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
259
271
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
260
- | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
272
+ | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
273
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
261
274
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
262
275
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
263
276
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
264
277
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
278
+ | `profile:` (top-level) | Names a bundled toolchain profile (`python`, `dotnet-node`) or a consumer profile file path. `load_config` merges the profile at load time: isolation key-wise with the pipeline winning, caches unioned profile-first. Absent = today's behavior. |
265
279
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
266
280
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
267
281
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
@@ -272,17 +286,25 @@ steps:
272
286
 
273
287
  ### Reasoning effort (opt-in)
274
288
 
275
- `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
276
- on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
277
- or `none`; unset means the field is never passed and the SDK default
278
- (`high`) applies, so a pipeline that omits it behaves exactly as before. A
279
- step's value overrides the `llm` block's value, so the review lane can hold
280
- a higher effort than the rest.
281
-
282
- The value is forwarded only for models the SDK/LiteLLM mark as supporting
283
- it; an unsupported value fails at the first LLM call (the schema rejects
284
- unknown names at load time, but cannot know provider capability). No
285
- pipeline ships a value: adopt one only after an observed run proves lower
289
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
290
+ named `llms:` profile, and individual agent steps. It takes one of `low`,
291
+ `medium`, `high`, `xhigh`, or `none`; unset means reasoning is off - the
292
+ field is forwarded as none, reasoning is stripped from requests, and a
293
+ thinking model whose history still carries reasoning fails fast with an
294
+ actionable engine error rather than a provider 400. A step's value overrides
295
+ the effective profile's value, so the review lane can hold a higher effort
296
+ than the rest, or a named profile can move an entire lane to a different
297
+ provider.
298
+
299
+ Reasoning is opt-in and provider-generic: any openai-compatible endpoint
300
+ expressible as an `LlmConfig` works, and reasoning passback is gated on the
301
+ configured effort level, never on a model-name list. A thinking model without
302
+ an effort level (or with `none` where the provider ignores it) is invalid
303
+ config - it still reasons but its API requires the passback the engine strips,
304
+ so the engine raises before the doomed request is sent. Set
305
+ `reasoning_effort` on the step or its profile, or use a non-thinking model.
306
+ The schema rejects unknown effort names at load time, but cannot know provider
307
+ capability; adopt an effort value only after an observed run proves lower
286
308
  per-turn latency with a green suite and no provider 400s.
287
309
 
288
310
  Recommended lane defaults (guidance only, not enforced):
@@ -425,12 +447,39 @@ console.
425
447
 
426
448
  ## Authoring a loop for your own repo
427
449
 
428
- A consumer repo needs two things written by hand - a pipeline file and the
429
- agent files it names - both committed before the first run (the host-boundary
430
- check refuses dirty tooling, and the `commit` step never commits it). Two
431
- reference layouts ship today: this repo dogfoods the engine on itself
432
- (python + pytest, no frontend, CLI-only qa), and the consumer repo carries
433
- the wider shape (.NET + npm frontend + a networked browser-probing qa step).
450
+ ### Start with `agentic-dev-loop init`
451
+
452
+ `init` writes the onboarding surface from a bundled toolchain profile:
453
+
454
+ ```bash
455
+ agentic-dev-loop init --profile python # or: --profile dotnet-node
456
+ ```
457
+
458
+ It detects solution files, `package.json` directories, lockfiles, README, and
459
+ AGENTS.md, asks only what detection cannot answer, and writes
460
+ `agentic-dev-loop/pipeline.yaml`, the three agent files, an executable
461
+ `trigger-agentic-dev-loop.sh`, `agentic-dev-loop/.engine-version` (the exact
462
+ installed engine version), any missing premise files, gitignore additions, and
463
+ an env-file template. It refuses to overwrite an existing
464
+ `agentic-dev-loop/` directory. A consumer-authored profile file can be passed
465
+ by path instead of a bundled name.
466
+
467
+ Profiles carry the environment facts every app on a toolchain shares (image,
468
+ cpus, caches, premise files, gitignore lines); recipes carry the gate shape and
469
+ command vocabulary. The pipeline references a profile with the top-level
470
+ `profile:` key, and `load_config` merges it at load time: the pipeline wins on
471
+ scalars, caches union profile-first, and absence of the key behaves exactly as
472
+ before.
473
+
474
+ `agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
475
+ Hard failures abort the run (engine pin mismatch, engine runtime availability,
476
+ API version, git repo, `.env*` files); warnings print and continue. `doctor
477
+ --json` emits the same results mechanically. The trigger repairs a pin/install
478
+ mismatch exactly once, then fails hard.
479
+
480
+ A consumer repo needs a pipeline file and the agent files it names, both
481
+ committed before the first run (the host-boundary check refuses dirty tooling,
482
+ and the `commit` step never commits it). `init` writes them; the layout is:
434
483
 
435
484
  ```
436
485
  <repo>/agentic-dev-loop/pipeline.yaml
@@ -515,22 +564,22 @@ adapt per repo.
515
564
  Adaptation checklist for a new repo:
516
565
 
517
566
  1. `llm:` - model, `base_url`, `api_key_env`.
518
- 2. `isolation:` - toolchain image override (the consumer repo pins a
519
- toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
520
- only if a stage runs Chromium.
521
- 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
522
- (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
523
- `frontend-editor/node_modules`); caches are never synced back, and a
567
+ 2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
568
+ 3. `profile:` - name a bundled profile (`python`, `dotnet-node`) or a
569
+ consumer profile path. The profile supplies `isolation:` (image,
570
+ `seccomp_profile`, cpus) and `caches:`; pipeline keys override its scalars.
571
+ 4. `caches:` - profile-supplied toolchain caches plus any run-specific dirs
572
+ (`.nuget-packages`, `qa/.browsers`); caches are never synced back, and a
524
573
  step's `setup:` hook runs with them mounted so it can warm them.
525
- 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
526
- 5. `secrets:` - refs only, resolved from host env; each step opts in to the
574
+ 5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
575
+ 6. `secrets:` - refs only, resolved from host env; each step opts in to the
527
576
  refs it needs.
528
- 6. `egress:` - `presets: [package-registries]` for fetch steps;
577
+ 7. `egress:` - `presets: [package-registries]` for fetch steps;
529
578
  `class: browser` (broad-but-logged) for browser-tier qa only.
530
- 7. `deps_policy:` and `lockfiles:` - see
579
+ 8. `deps_policy:` and `lockfiles:` - see
531
580
  [Dependency policy](#dependency-policy-locked-by-default).
532
- 8. The dev prompt's impossible set - write it for your toolchain.
533
- 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
581
+ 9. The dev prompt's impossible set - write it for your toolchain.
582
+ 10. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
534
583
  docs: shared context for every agent step.
535
584
 
536
585
  Validate before the first run - dry-run loads and prints the resolved plan
@@ -158,6 +158,17 @@ llm:
158
158
  api_key_env: OPENAI_API_KEY
159
159
  # reasoning_effort: medium # optional: low | medium | high | xhigh | none
160
160
 
161
+ # Named LLM profiles (054): a step names one with `llm: <name>` to move the
162
+ # whole profile (model, endpoint, key, effort default) together - no partial
163
+ # merges. The `llm:` block above remains the default. Profile keys are
164
+ # host-side: sandbox access still goes through `secrets:`, never a profile key.
165
+ # llms:
166
+ # review:
167
+ # model: openai/deepseek-v4-pro
168
+ # base_url: https://api.deepseek.com
169
+ # api_key_env: REVIEW_API_KEY
170
+ # reasoning_effort: xhigh
171
+
161
172
  max_attempts: 10 # CLI --max-attempts overrides
162
173
  agents_dir: sdk_agents # your <name>.agent.md files
163
174
 
@@ -187,7 +198,8 @@ steps:
187
198
  agent: review
188
199
  artifact: review-findings.md
189
200
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
190
- # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
201
+ # reasoning_effort: xhigh # per-step override of the effective profile's effort
202
+ # llm: review # (054) select a named `llms:` profile for this step
191
203
  on_pass: qa
192
204
  on_fail: { goto: dev, retry: true }
193
205
 
@@ -213,11 +225,13 @@ steps:
213
225
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
214
226
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
215
227
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
216
- | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
228
+ | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
229
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
217
230
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
218
231
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
219
232
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
220
233
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
234
+ | `profile:` (top-level) | Names a bundled toolchain profile (`python`, `dotnet-node`) or a consumer profile file path. `load_config` merges the profile at load time: isolation key-wise with the pipeline winning, caches unioned profile-first. Absent = today's behavior. |
221
235
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
222
236
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
223
237
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
@@ -228,17 +242,25 @@ steps:
228
242
 
229
243
  ### Reasoning effort (opt-in)
230
244
 
231
- `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
232
- on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
233
- or `none`; unset means the field is never passed and the SDK default
234
- (`high`) applies, so a pipeline that omits it behaves exactly as before. A
235
- step's value overrides the `llm` block's value, so the review lane can hold
236
- a higher effort than the rest.
237
-
238
- The value is forwarded only for models the SDK/LiteLLM mark as supporting
239
- it; an unsupported value fails at the first LLM call (the schema rejects
240
- unknown names at load time, but cannot know provider capability). No
241
- pipeline ships a value: adopt one only after an observed run proves lower
245
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
246
+ named `llms:` profile, and individual agent steps. It takes one of `low`,
247
+ `medium`, `high`, `xhigh`, or `none`; unset means reasoning is off - the
248
+ field is forwarded as none, reasoning is stripped from requests, and a
249
+ thinking model whose history still carries reasoning fails fast with an
250
+ actionable engine error rather than a provider 400. A step's value overrides
251
+ the effective profile's value, so the review lane can hold a higher effort
252
+ than the rest, or a named profile can move an entire lane to a different
253
+ provider.
254
+
255
+ Reasoning is opt-in and provider-generic: any openai-compatible endpoint
256
+ expressible as an `LlmConfig` works, and reasoning passback is gated on the
257
+ configured effort level, never on a model-name list. A thinking model without
258
+ an effort level (or with `none` where the provider ignores it) is invalid
259
+ config - it still reasons but its API requires the passback the engine strips,
260
+ so the engine raises before the doomed request is sent. Set
261
+ `reasoning_effort` on the step or its profile, or use a non-thinking model.
262
+ The schema rejects unknown effort names at load time, but cannot know provider
263
+ capability; adopt an effort value only after an observed run proves lower
242
264
  per-turn latency with a green suite and no provider 400s.
243
265
 
244
266
  Recommended lane defaults (guidance only, not enforced):
@@ -381,12 +403,39 @@ console.
381
403
 
382
404
  ## Authoring a loop for your own repo
383
405
 
384
- A consumer repo needs two things written by hand - a pipeline file and the
385
- agent files it names - both committed before the first run (the host-boundary
386
- check refuses dirty tooling, and the `commit` step never commits it). Two
387
- reference layouts ship today: this repo dogfoods the engine on itself
388
- (python + pytest, no frontend, CLI-only qa), and the consumer repo carries
389
- the wider shape (.NET + npm frontend + a networked browser-probing qa step).
406
+ ### Start with `agentic-dev-loop init`
407
+
408
+ `init` writes the onboarding surface from a bundled toolchain profile:
409
+
410
+ ```bash
411
+ agentic-dev-loop init --profile python # or: --profile dotnet-node
412
+ ```
413
+
414
+ It detects solution files, `package.json` directories, lockfiles, README, and
415
+ AGENTS.md, asks only what detection cannot answer, and writes
416
+ `agentic-dev-loop/pipeline.yaml`, the three agent files, an executable
417
+ `trigger-agentic-dev-loop.sh`, `agentic-dev-loop/.engine-version` (the exact
418
+ installed engine version), any missing premise files, gitignore additions, and
419
+ an env-file template. It refuses to overwrite an existing
420
+ `agentic-dev-loop/` directory. A consumer-authored profile file can be passed
421
+ by path instead of a bundled name.
422
+
423
+ Profiles carry the environment facts every app on a toolchain shares (image,
424
+ cpus, caches, premise files, gitignore lines); recipes carry the gate shape and
425
+ command vocabulary. The pipeline references a profile with the top-level
426
+ `profile:` key, and `load_config` merges it at load time: the pipeline wins on
427
+ scalars, caches union profile-first, and absence of the key behaves exactly as
428
+ before.
429
+
430
+ `agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
431
+ Hard failures abort the run (engine pin mismatch, engine runtime availability,
432
+ API version, git repo, `.env*` files); warnings print and continue. `doctor
433
+ --json` emits the same results mechanically. The trigger repairs a pin/install
434
+ mismatch exactly once, then fails hard.
435
+
436
+ A consumer repo needs a pipeline file and the agent files it names, both
437
+ committed before the first run (the host-boundary check refuses dirty tooling,
438
+ and the `commit` step never commits it). `init` writes them; the layout is:
390
439
 
391
440
  ```
392
441
  <repo>/agentic-dev-loop/pipeline.yaml
@@ -471,22 +520,22 @@ adapt per repo.
471
520
  Adaptation checklist for a new repo:
472
521
 
473
522
  1. `llm:` - model, `base_url`, `api_key_env`.
474
- 2. `isolation:` - toolchain image override (the consumer repo pins a
475
- toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
476
- only if a stage runs Chromium.
477
- 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
478
- (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
479
- `frontend-editor/node_modules`); caches are never synced back, and a
523
+ 2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
524
+ 3. `profile:` - name a bundled profile (`python`, `dotnet-node`) or a
525
+ consumer profile path. The profile supplies `isolation:` (image,
526
+ `seccomp_profile`, cpus) and `caches:`; pipeline keys override its scalars.
527
+ 4. `caches:` - profile-supplied toolchain caches plus any run-specific dirs
528
+ (`.nuget-packages`, `qa/.browsers`); caches are never synced back, and a
480
529
  step's `setup:` hook runs with them mounted so it can warm them.
481
- 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
482
- 5. `secrets:` - refs only, resolved from host env; each step opts in to the
530
+ 5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
531
+ 6. `secrets:` - refs only, resolved from host env; each step opts in to the
483
532
  refs it needs.
484
- 6. `egress:` - `presets: [package-registries]` for fetch steps;
533
+ 7. `egress:` - `presets: [package-registries]` for fetch steps;
485
534
  `class: browser` (broad-but-logged) for browser-tier qa only.
486
- 7. `deps_policy:` and `lockfiles:` - see
535
+ 8. `deps_policy:` and `lockfiles:` - see
487
536
  [Dependency policy](#dependency-policy-locked-by-default).
488
- 8. The dev prompt's impossible set - write it for your toolchain.
489
- 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
537
+ 9. The dev prompt's impossible set - write it for your toolchain.
538
+ 10. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
490
539
  docs: shared context for every agent step.
491
540
 
492
541
  Validate before the first run - dry-run loads and prints the resolved plan
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "0.9.2"
7
+ version = "0.10.2"
8
8
  description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -76,8 +76,10 @@ where = ["src"]
76
76
 
77
77
  [tool.setuptools.package-data]
78
78
  agentic_dev_loop = ["py.typed"]
79
+ "agentic_dev_loop.core.profiles" = ["data/*.yaml"]
79
80
  "agentic_dev_loop.engine" = ["*.md"]
80
81
  "agentic_dev_loop.engine.seccomp" = ["*.json"]
82
+ "agentic_dev_loop.engine.onboarding" = ["recipes/*.yaml", "templates/*"]
81
83
 
82
84
  [tool.pytest.ini_options]
83
85
  markers = [
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
+ import importlib.metadata
6
7
  import re
7
8
  import warnings
8
9
  from pathlib import Path
@@ -396,9 +397,13 @@ class StepConfig(BaseModel):
396
397
  terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
397
398
  finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
398
399
  brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
399
- # 047 (D7): per-step opt-in override; unset falls back to the llm block,
400
- # then the SDK default. Full SDK literal, passthrough semantics.
400
+ # 047 (D7) / 054 (D6): per-step opt-in override; unset falls back to the
401
+ # effective profile's value (the named `llms:` profile, else the `llm:`
402
+ # block), else reasoning is off. Full SDK literal, passthrough semantics.
401
403
  reasoning_effort: ReasoningEffort | None = None
404
+ # 054 (D3): names a top-level `llms:` profile; the whole profile moves
405
+ # together (model, endpoint, key, effort default). None = the `llm:` block.
406
+ llm: str | None = None
402
407
  custom_prompt: str | None = None # agent steps only (040); file path resolved at load
403
408
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
404
409
  setup: str | None = None # command or agent steps; runs fresh at each step entry (052)
@@ -486,6 +491,16 @@ class StepConfig(BaseModel):
486
491
  f"step {self.name!r}: exactly one of 'agent', 'action', "
487
492
  "'command', or 'brief_inputs' is required"
488
493
  )
494
+ if self.llm is not None:
495
+ if not self.llm.strip():
496
+ raise ValueError(
497
+ f"step {self.name!r}: 'llm' must be a non-empty profile name"
498
+ )
499
+ if self.kind not in ("agent", "brief"):
500
+ raise ValueError(
501
+ f"step {self.name!r}: 'llm' names an LLM profile and is "
502
+ "only allowed on 'agent' or 'brief_inputs' steps"
503
+ )
489
504
  if self.brief_inputs is not None:
490
505
  if self.produces:
491
506
  raise ValueError(
@@ -587,9 +602,9 @@ class LlmConfig(BaseModel):
587
602
  base_url: str | None = None
588
603
  api_key_env: str
589
604
  timeout: float | None = None # omitted → opinionated no-timeout default
590
- # 047 (D1): opt-in passthrough; unset means the field is not passed at
591
- # all, so the SDK default ("high") applies and today's behavior is
592
- # byte-identical. No default may move without an observed run (D6).
605
+ # 047 (D1) / 054 (D6): opt-in passthrough; unset means reasoning is off
606
+ # (the engine forwards none and strips reasoning passback). The SDK
607
+ # "high" default never engages because build_llm always passes the value.
593
608
  reasoning_effort: ReasoningEffort | None = None
594
609
 
595
610
 
@@ -606,6 +621,9 @@ class PipelineConfig(BaseModel):
606
621
  extensions_dir: str = "~/.agentic-dev-loop/extensions"
607
622
  env_allowlist: list[str] = []
608
623
  llm: LlmConfig
624
+ # 054 (D1): named LLM profiles, each a strict LlmConfig. Optional and
625
+ # purely additive - the `llm:` block above remains the default.
626
+ llms: dict[str, LlmConfig] = {}
609
627
  isolation: IsolationConfig | None = None # None = always-on defaults
610
628
  state_dir: str = ".pr" # default relocated to ~/.agentic-dev-loop/runs/<hash>/ at load
611
629
  secrets: dict[str, SecretSpec] = {} # ref name -> locator; values never appear here
@@ -630,6 +648,9 @@ class PipelineConfig(BaseModel):
630
648
  # edit instead of raising. Same declared-path vocabulary as
631
649
  # ``sync_excludes:``.
632
650
  merge_on_conflict: list[str] = []
651
+ # 055: the toolchain profile resolved at load time. Kept on the validated
652
+ # config so the run records which profile supplied its defaults.
653
+ profile: str | None = None
633
654
 
634
655
  @field_validator("deps_policy")
635
656
  @classmethod
@@ -690,6 +711,23 @@ class PipelineConfig(BaseModel):
690
711
  "enforces, or set 'deps_policy: open'")
691
712
  return self
692
713
 
714
+ @model_validator(mode="after")
715
+ def _check_llm_profiles(self) -> "PipelineConfig":
716
+ """054 (D5): the registry is additive and referential. A profile
717
+ literally named 'default' is rejected (the `llm:` block already is the
718
+ default), and any step naming an undeclared profile fails at load,
719
+ never mid-run."""
720
+ if "default" in self.llms:
721
+ raise ValueError(
722
+ "'llms' cannot declare a profile named 'default' - the "
723
+ "top-level 'llm:' block is already the default")
724
+ for step in self.steps:
725
+ if step.llm is not None and step.llm not in self.llms:
726
+ raise ValueError(
727
+ f"step {step.name!r}: unknown LLM profile {step.llm!r} - "
728
+ f"declared profiles: {sorted(self.llms)}")
729
+ return self
730
+
693
731
  @model_validator(mode="after")
694
732
  def _mounts_avoid_state_dir(self) -> "PipelineConfig":
695
733
  """BR-1.3: the host-only run-state dir is never a mount path.
@@ -858,6 +896,21 @@ def load_config(path: Path) -> PipelineConfig:
858
896
  raise ValueError(
859
897
  f"pipeline config must be a mapping, got {type(data).__name__}: {path}"
860
898
  )
899
+ profile_ref = data.pop("profile", None)
900
+ if profile_ref is not None:
901
+ from agentic_dev_loop.core.profiles import (
902
+ ProfileError,
903
+ load_profile,
904
+ merge_profile,
905
+ )
906
+
907
+ try:
908
+ installed = importlib.metadata.version("agentic-dev-loop")
909
+ profile = load_profile(str(profile_ref), installed)
910
+ data = merge_profile(data, profile)
911
+ except ProfileError as exc:
912
+ raise ValueError(f"pipeline {path}: {exc}") from exc
913
+ data["profile"] = profile_ref
861
914
  if "state_dir" not in data:
862
915
  data["state_dir"] = default_state_dir(path)
863
916
  cfg = PipelineConfig.model_validate(data)