agentic-dev-loop 1.0.0a44__tar.gz → 1.0.0a46__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.

Potentially problematic release.


This version of agentic-dev-loop might be problematic. Click here for more details.

Files changed (104) hide show
  1. {agentic_dev_loop-1.0.0a44/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a46}/PKG-INFO +37 -8
  2. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/README.md +36 -7
  3. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/config.py +40 -2
  5. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/agents.py +19 -0
  6. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/brief_coverage.py +7 -18
  7. agentic_dev_loop-1.0.0a46/src/agentic_dev_loop/engine/brief_prompt.md +46 -0
  8. agentic_dev_loop-1.0.0a46/src/agentic_dev_loop/engine/brief_prompt.py +47 -0
  9. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/dryrun.py +9 -5
  10. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/llm.py +14 -7
  11. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/stages.py +43 -5
  12. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46/src/agentic_dev_loop.egg-info}/PKG-INFO +37 -8
  13. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/SOURCES.txt +3 -0
  14. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_033_brief_step.py +44 -6
  15. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_041_brief_coverage.py +27 -16
  16. agentic_dev_loop-1.0.0a46/tests/test_046_brief_prompt.py +45 -0
  17. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_agents.py +20 -1
  18. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_config.py +34 -0
  19. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_e2e_fixtures.py +1 -1
  20. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_llm.py +40 -0
  21. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_publish.py +39 -0
  22. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_trigger_script.py +49 -4
  23. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/LICENSE +0 -0
  24. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/setup.cfg +0 -0
  25. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/__init__.py +0 -0
  26. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/__init__.py +0 -0
  27. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/paths.py +0 -0
  28. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/results.py +0 -0
  29. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/verdict.py +0 -0
  30. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/__init__.py +0 -0
  31. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/actions.py +0 -0
  32. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  33. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  34. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  35. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/cli.py +0 -0
  36. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/containers.py +0 -0
  37. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/deps.py +0 -0
  38. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  39. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  40. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  41. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  42. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  43. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/graph.py +0 -0
  44. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  45. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/isolation.py +0 -0
  46. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/loaders.py +0 -0
  47. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/mounts.py +0 -0
  48. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/outputs.py +0 -0
  49. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/presets.py +0 -0
  50. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/proc.py +0 -0
  51. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/registry.py +0 -0
  52. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  53. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  54. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/secrets.py +0 -0
  55. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  56. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  57. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/py.typed +0 -0
  58. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/viz/__init__.py +0 -0
  59. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/viz/__main__.py +0 -0
  60. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/viz/server.py +0 -0
  61. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  62. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  63. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  64. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  65. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_028_declared_caches.py +0 -0
  66. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_029_merge_on_conflict.py +0 -0
  67. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_035_finalize_nudge.py +0 -0
  68. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  69. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_actions.py +0 -0
  70. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_agent_error_guide.py +0 -0
  71. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_aidlc.py +0 -0
  72. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_cli.py +0 -0
  73. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_containers.py +0 -0
  74. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_deps.py +0 -0
  75. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_dry_run.py +0 -0
  76. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_egress_proxy.py +0 -0
  77. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_graph.py +0 -0
  78. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_hostboundary.py +0 -0
  79. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_hostboundary_integration.py +0 -0
  80. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_isolation.py +0 -0
  81. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_isolation_integration.py +0 -0
  82. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_isolation_pbt.py +0 -0
  83. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_loaders.py +0 -0
  84. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_module_contract.py +0 -0
  85. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_outputs.py +0 -0
  86. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_outputs_pbt.py +0 -0
  87. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_proc.py +0 -0
  88. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_qa_prompt.py +0 -0
  89. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_registry.py +0 -0
  90. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_repo_ascii.py +0 -0
  91. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_results.py +0 -0
  92. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_sdk_compat.py +0 -0
  93. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_secrets.py +0 -0
  94. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_secrets_integration.py +0 -0
  95. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_secrets_pipeline.py +0 -0
  96. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_source_resolution.py +0 -0
  97. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_stages.py +0 -0
  98. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_step_workspace.py +0 -0
  99. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_sync_back.py +0 -0
  100. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_unit5_config.py +0 -0
  101. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_unit5_pbt.py +0 -0
  102. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_unit5_pure.py +0 -0
  103. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_verdict.py +0 -0
  104. {agentic_dev_loop-1.0.0a44 → agentic_dev_loop-1.0.0a46}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a44
3
+ Version: 1.0.0a46
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
@@ -199,6 +199,7 @@ llm:
199
199
  model: openai/deepseek-v4-pro
200
200
  base_url: https://api.deepseek.com
201
201
  api_key_env: OPENAI_API_KEY
202
+ # reasoning_effort: medium # optional: low | medium | high | xhigh | none
202
203
 
203
204
  max_attempts: 10 # CLI --max-attempts overrides
204
205
  agents_dir: sdk_agents # your <name>.agent.md files
@@ -228,6 +229,7 @@ steps:
228
229
  agent: review
229
230
  artifact: review-findings.md
230
231
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
232
+ # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
231
233
  on_pass: qa
232
234
  on_fail: { goto: dev, retry: true }
233
235
 
@@ -249,10 +251,11 @@ steps:
249
251
  | --- | --- |
250
252
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
251
253
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
252
- | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
254
+ | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
253
255
  | `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. |
254
256
  | `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). |
255
257
  | `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. |
258
+ | `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). |
256
259
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
257
260
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
258
261
  | `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
@@ -266,17 +269,43 @@ steps:
266
269
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
267
270
  | `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
268
271
 
272
+ ### Reasoning effort (opt-in)
273
+
274
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
275
+ on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
276
+ or `none`; unset means the field is never passed and the SDK default
277
+ (`high`) applies, so a pipeline that omits it behaves exactly as before. A
278
+ step's value overrides the `llm` block's value, so the review lane can hold
279
+ a higher effort than the rest.
280
+
281
+ The value is forwarded only for models the SDK/LiteLLM mark as supporting
282
+ it; an unsupported value fails at the first LLM call (the schema rejects
283
+ unknown names at load time, but cannot know provider capability). No
284
+ pipeline ships a value: adopt one only after an observed run proves lower
285
+ per-turn latency with a green suite and no provider 400s.
286
+
287
+ Recommended lane defaults (guidance only, not enforced):
288
+
289
+ | Lane | reasoning_effort | Why |
290
+ | --- | --- | --- |
291
+ | brief | medium | digest and classify, no design work |
292
+ | dev | high | hold the line: dev caught a real plan bug and a real sandbox triage this unit |
293
+ | qa | medium | classification discipline must stay reliable |
294
+ | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
295
+
269
296
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
270
297
  run start (or when its inputs change), its `produces:` artifact survives attempt
271
298
  starts, and the dev/review agents read that digest instead of re-deriving repo
272
299
  rules and environment facts every attempt.
273
300
 
274
- The brief digest is mechanically coverage-checked: the four section headings
275
- (`Repo rules digest`, `Unit scope and lane digest`, `Environment facts`, and
276
- `Open risks`) must appear, every anchor extracted from the `include` files
277
- (markdown `##` headings, and YAML category names plus `enforcement_levels`)
278
- must appear on its own line (case-insensitive, whitespace-collapsed, `**`
279
- decoration allowed), and every `env_facts` entry must appear verbatim. A miss
301
+ The brief digest is mechanically coverage-checked: the configured
302
+ `brief_inputs.sections` headings must each appear on their own line, every
303
+ anchor extracted from the `include` files (markdown `##` headings, and YAML
304
+ category names plus `enforcement_levels`) must appear on its own line
305
+ (case-insensitive, whitespace-collapsed, `**` decoration allowed), and every
306
+ `env_facts` entry must appear verbatim. The section list feeds both the
307
+ engine-embedded brief prompt and the coverage gate, so the two can never
308
+ disagree; a miss
280
309
  re-runs the brief agent once per missing set with a `## Coverage retry`
281
310
  correction note, bounded to three retries per input set; if the same items are
282
311
  still dropped after the cap, the run fails and `last-error.md` names the
@@ -155,6 +155,7 @@ llm:
155
155
  model: openai/deepseek-v4-pro
156
156
  base_url: https://api.deepseek.com
157
157
  api_key_env: OPENAI_API_KEY
158
+ # reasoning_effort: medium # optional: low | medium | high | xhigh | none
158
159
 
159
160
  max_attempts: 10 # CLI --max-attempts overrides
160
161
  agents_dir: sdk_agents # your <name>.agent.md files
@@ -184,6 +185,7 @@ steps:
184
185
  agent: review
185
186
  artifact: review-findings.md
186
187
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
188
+ # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
187
189
  on_pass: qa
188
190
  on_fail: { goto: dev, retry: true }
189
191
 
@@ -205,10 +207,11 @@ steps:
205
207
  | --- | --- |
206
208
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
207
209
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
208
- | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
210
+ | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
209
211
  | `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. |
210
212
  | `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). |
211
213
  | `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. |
214
+ | `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). |
212
215
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
213
216
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
214
217
  | `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
@@ -222,17 +225,43 @@ steps:
222
225
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
223
226
  | `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
224
227
 
228
+ ### Reasoning effort (opt-in)
229
+
230
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
231
+ on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
232
+ or `none`; unset means the field is never passed and the SDK default
233
+ (`high`) applies, so a pipeline that omits it behaves exactly as before. A
234
+ step's value overrides the `llm` block's value, so the review lane can hold
235
+ a higher effort than the rest.
236
+
237
+ The value is forwarded only for models the SDK/LiteLLM mark as supporting
238
+ it; an unsupported value fails at the first LLM call (the schema rejects
239
+ unknown names at load time, but cannot know provider capability). No
240
+ pipeline ships a value: adopt one only after an observed run proves lower
241
+ per-turn latency with a green suite and no provider 400s.
242
+
243
+ Recommended lane defaults (guidance only, not enforced):
244
+
245
+ | Lane | reasoning_effort | Why |
246
+ | --- | --- | --- |
247
+ | brief | medium | digest and classify, no design work |
248
+ | dev | high | hold the line: dev caught a real plan bug and a real sandbox triage this unit |
249
+ | qa | medium | classification discipline must stay reliable |
250
+ | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
251
+
225
252
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
226
253
  run start (or when its inputs change), its `produces:` artifact survives attempt
227
254
  starts, and the dev/review agents read that digest instead of re-deriving repo
228
255
  rules and environment facts every attempt.
229
256
 
230
- The brief digest is mechanically coverage-checked: the four section headings
231
- (`Repo rules digest`, `Unit scope and lane digest`, `Environment facts`, and
232
- `Open risks`) must appear, every anchor extracted from the `include` files
233
- (markdown `##` headings, and YAML category names plus `enforcement_levels`)
234
- must appear on its own line (case-insensitive, whitespace-collapsed, `**`
235
- decoration allowed), and every `env_facts` entry must appear verbatim. A miss
257
+ The brief digest is mechanically coverage-checked: the configured
258
+ `brief_inputs.sections` headings must each appear on their own line, every
259
+ anchor extracted from the `include` files (markdown `##` headings, and YAML
260
+ category names plus `enforcement_levels`) must appear on its own line
261
+ (case-insensitive, whitespace-collapsed, `**` decoration allowed), and every
262
+ `env_facts` entry must appear verbatim. The section list feeds both the
263
+ engine-embedded brief prompt and the coverage gate, so the two can never
264
+ disagree; a miss
236
265
  re-runs the brief agent once per missing set with a `## Coverage retry`
237
266
  correction note, bounded to three retries per input set; if the same items are
238
267
  still dropped after the cap, the run fails and `last-error.md` names the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a44"
7
+ version = "1.0.0a46"
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"
@@ -6,7 +6,7 @@ import hashlib
6
6
  import re
7
7
  import warnings
8
8
  from pathlib import Path
9
- from typing import Any
9
+ from typing import Any, Literal
10
10
 
11
11
  import yaml
12
12
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
@@ -25,6 +25,21 @@ HOST_ONLY_MOUNT_DIRS = frozenset({".venv"})
25
25
  KNOWN_EGRESS_PRESETS = frozenset({"package-registries"})
26
26
  EGRESS_CLASSES = frozenset({"allowlist", "browser"})
27
27
 
28
+ # The neutral default section headings a brief step must emit (046). A
29
+ # consumer overrides them via `brief_inputs.sections`; the coverage gate
30
+ # and the embedded brief prompt both render from this list.
31
+ DEFAULT_BRIEF_SECTIONS = [
32
+ "Repo rules digest",
33
+ "Unit scope digest",
34
+ "Environment facts",
35
+ "Open risks",
36
+ ]
37
+
38
+ # 047: the full SDK reasoning_effort literal, passed through verbatim. The
39
+ # engine is provider-generic, so it validates shape only - whether a given
40
+ # provider accepts every value (notably "none") is operator risk (D3).
41
+ ReasoningEffort = Literal["low", "medium", "high", "xhigh", "none"]
42
+
28
43
 
29
44
  def _validate_mount_path(path: str) -> str:
30
45
  """Validate one repo-relative mount path (BR-1.3).
@@ -318,8 +333,9 @@ class BriefInputsSpec(BaseModel):
318
333
 
319
334
  include: list[str] = []
320
335
  env_facts: list[str] = []
336
+ sections: list[str] = DEFAULT_BRIEF_SECTIONS
321
337
 
322
- @field_validator("include", "env_facts", mode="before")
338
+ @field_validator("include", "env_facts", "sections", mode="before")
323
339
  @classmethod
324
340
  def _coerce_list(cls, v: Any) -> Any:
325
341
  return [v] if isinstance(v, str) else v
@@ -338,6 +354,21 @@ class BriefInputsSpec(BaseModel):
338
354
  "brief_inputs.env_facts entries must be non-empty strings")
339
355
  return v
340
356
 
357
+ @field_validator("sections")
358
+ @classmethod
359
+ def _valid_sections(cls, v: list[str]) -> list[str]:
360
+ for section in v:
361
+ if not isinstance(section, str) or not section.strip():
362
+ raise ValueError(
363
+ "brief_inputs.sections entries must be non-empty strings")
364
+ if not v:
365
+ raise ValueError(
366
+ "brief_inputs.sections must be a non-empty list of headings")
367
+ if len(set(v)) != len(v):
368
+ raise ValueError(
369
+ "brief_inputs.sections entries must be unique")
370
+ return v
371
+
341
372
 
342
373
  class StepConfig(BaseModel):
343
374
  """One pipeline step. Its type IS the key it carries: exactly one of
@@ -359,6 +390,9 @@ class StepConfig(BaseModel):
359
390
  terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
360
391
  finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
361
392
  brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
393
+ # 047 (D7): per-step opt-in override; unset falls back to the llm block,
394
+ # then the SDK default. Full SDK literal, passthrough semantics.
395
+ reasoning_effort: ReasoningEffort | None = None
362
396
  custom_prompt: str | None = None # agent steps only (040); file path resolved at load
363
397
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
364
398
  setup: str | None = None # BR-13: command steps only
@@ -526,6 +560,10 @@ class LlmConfig(BaseModel):
526
560
  base_url: str | None = None
527
561
  api_key_env: str
528
562
  timeout: float | None = None # omitted → opinionated no-timeout default
563
+ # 047 (D1): opt-in passthrough; unset means the field is not passed at
564
+ # all, so the SDK default ("high") applies and today's behavior is
565
+ # byte-identical. No default may move without an observed run (D6).
566
+ reasoning_effort: ReasoningEffort | None = None
529
567
 
530
568
 
531
569
  class PipelineConfig(BaseModel):
@@ -16,6 +16,7 @@ from openhands.tools.task_tracker import TaskTrackerTool # noqa: F401 - registe
16
16
  from openhands.tools.terminal import TerminalTool # noqa: F401 - registers "terminal"
17
17
 
18
18
  from .forbidden import ForbiddenRule, match_forbidden_command
19
+ from .brief_prompt import render_brief_prompt
19
20
  from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
20
21
  resolve_sandbox_env)
21
22
 
@@ -59,6 +60,24 @@ def load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
59
60
  return agent_definition_to_factory(agent_def)(llm)
60
61
 
61
62
 
63
+ def load_brief_agent(name: str, sections: list[str], llm: LLM) -> "Agent":
64
+ """Build the brief agent from the engine-embedded prompt (046).
65
+
66
+ The brief prompt is engine-owned and versioned; it is never loaded from
67
+ ``agents_dir``. The OUTPUT contract's headings are rendered from the
68
+ configured ``brief_inputs.sections``, so the prompt and the coverage gate
69
+ share one list and cannot diverge (043)."""
70
+ from openhands.sdk.subagent.registry import agent_definition_to_factory
71
+ from openhands.sdk.subagent.schema import AgentDefinition
72
+
73
+ agent_def = AgentDefinition(
74
+ name=name,
75
+ tools=["terminal"],
76
+ system_prompt=render_brief_prompt(sections),
77
+ )
78
+ return agent_definition_to_factory(agent_def)(llm)
79
+
80
+
62
81
  class SandboxTerminalExecutor(ToolExecutor):
63
82
  """A stateless terminal executor: each action is a fresh ``Sandbox.run``
64
83
  (no persistent shell session). ``is_input`` degrades to an unsupported
@@ -12,13 +12,6 @@ from __future__ import annotations
12
12
 
13
13
  import re
14
14
 
15
- _REQUIRED_SECTIONS = (
16
- "Repo rules digest",
17
- "Unit scope and lane digest",
18
- "Environment facts",
19
- "Open risks",
20
- )
21
-
22
15
 
23
16
  def _normalize(text: str) -> str:
24
17
  """Case-fold and collapse whitespace - the matching normal form."""
@@ -38,11 +31,6 @@ def _normalize_line(line: str) -> str:
38
31
  return _normalize(stripped)
39
32
 
40
33
 
41
- def required_sections() -> list[str]:
42
- """The four template headings the brief must emit."""
43
- return list(_REQUIRED_SECTIONS)
44
-
45
-
46
34
  def extract_anchors(content: str) -> list[str]:
47
35
  """Derive the mechanical anchor set from one include file.
48
36
 
@@ -75,16 +63,17 @@ def extract_anchors(content: str) -> list[str]:
75
63
 
76
64
 
77
65
  def missing_anchors(brief_text: str, anchors: list[str],
78
- env_facts: list[str]) -> list[str]:
66
+ env_facts: list[str], sections: list[str]) -> list[str]:
79
67
  """Return every required item the captured brief fails to cover.
80
68
 
81
- Required section headings and anchors are matched line-anchored
82
- (case-insensitive, whitespace-collapsed, ``**`` decoration stripped). Env
83
- facts are matched verbatim, exactly as handed to the brief agent. An empty
84
- list means the digest passes coverage."""
69
+ Required section headings (from the configured ``brief_inputs.sections``)
70
+ and anchors are matched line-anchored (case-insensitive,
71
+ whitespace-collapsed, ``**`` decoration stripped). Env facts are matched
72
+ verbatim, exactly as handed to the brief agent. An empty list means the
73
+ digest passes coverage."""
85
74
  missing: list[str] = []
86
75
  lines = {_normalize_line(line) for line in brief_text.splitlines()}
87
- for section in _REQUIRED_SECTIONS:
76
+ for section in sections:
88
77
  if _normalize(section) not in lines:
89
78
  missing.append(section)
90
79
  for anchor in anchors:
@@ -0,0 +1,46 @@
1
+ # Brief Agent
2
+
3
+ You are the per-run context digester for a gated construction loop. You run
4
+ once per run (or once when the inputs change) and turn a closed input set into
5
+ a compact brief that the dev and review agents reuse on every attempt.
6
+
7
+ ## WORKFLOW
8
+
9
+ 1. Read every input section in the task message below. The inputs are the
10
+ resolved unit/plan docs, the listed repo-rule files, and the
11
+ `Environment facts` block. That is the closed list - nothing else.
12
+ 2. Digest the inputs into the configured sections named in OUTPUT below.
13
+ Digest, do not transcribe: the brief is compact working memory, not a copy
14
+ of the inputs.
15
+ 3. State the brief in your final response. The pipeline captures it as
16
+ `attempt-brief.md`.
17
+
18
+ ## RULES
19
+
20
+ - One pass, no exploration. Read the listed inputs, emit the brief, finish.
21
+ Do not explore, do not verify, do not run commands, and do not read anything
22
+ not listed in the task message. Your inputs are already embedded in the task
23
+ message; the terminal is available only if a listed input must be re-read.
24
+ - Prior verdicts and feedback are deliberately absent from your inputs. They
25
+ reach the dev and review agents through a separate notes channel every
26
+ attempt. Do not invent them and do not re-derive them.
27
+ - The repo-rules digest is structural: one line per top-level section of each
28
+ repo-rule input. Markdown anchors are the `##` headings: keep each heading
29
+ verbatim, then compress that section's rules into the one line that follows
30
+ it. YAML anchors are each `- name:` entry under `categories:` plus the
31
+ `enforcement_levels` preamble: keep each name verbatim. No top-level section
32
+ or category may be dropped; merging is allowed only between a section and
33
+ its own subsections. The anchor line holds the heading and nothing else; the
34
+ compressed rules go on the following line:
35
+
36
+ - general_principles
37
+ Simplest thing that works; defer decisions to the last responsible moment.
38
+
39
+ A line like `general_principles: Simplest thing that works; ...` does not
40
+ satisfy coverage; the heading must be the only text on its line.
41
+ - Keep the environment-facts entries verbatim: each fact and the behavior it
42
+ implies travel together.
43
+ - ASCII only. Do not use em dash characters.
44
+ - If the task message carries a `## Coverage retry` section, regenerate the
45
+ brief so every listed item appears, verbatim where the contract requires
46
+ verbatim. No exploration: the listed items are the only correction signal.
@@ -0,0 +1,47 @@
1
+ """Bundled brief-agent prompt (046).
2
+
3
+ The brief prompt is an engine-owned resource, versioned with the engine so the
4
+ prompt contract and the coverage gate cannot drift (the 043 divergence class).
5
+ The static prose is packaged and read at runtime like the 034 error guide; the
6
+ section-dependent OUTPUT block is rendered here from the configured
7
+ ``brief_inputs.sections`` list, so one list feeds the prompt and the gate.
8
+
9
+ ``BRIEF_PROMPT_VERSION`` joins the brief digest hash: an engine upgrade (or a
10
+ sections edit) busts a cached digest and forces a fresh capture.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from importlib.resources import files
16
+
17
+ BRIEF_PROMPT_RESOURCE = "brief_prompt.md"
18
+ BRIEF_PROMPT_VERSION = "1"
19
+
20
+
21
+ def load_template() -> str:
22
+ """The static brief-prompt prose (header, workflow, rules)."""
23
+ return (files("agentic_dev_loop.engine")
24
+ .joinpath(BRIEF_PROMPT_RESOURCE)
25
+ .read_text(encoding="utf-8"))
26
+
27
+
28
+ def _output_block(sections: list[str]) -> str:
29
+ """Render the OUTPUT contract from the configured section headings."""
30
+ headings = "\n\n".join(f"## {section}" for section in sections)
31
+ names = ", ".join(f"`## {section}`" for section in sections)
32
+ return (
33
+ "## OUTPUT\n\n"
34
+ "Start your final response with exactly this structure. Each configured "
35
+ "section heading appears as its own `## ` line, followed by the "
36
+ "compressed content for that section (write `None` when a section has "
37
+ "nothing):\n\n"
38
+ f"{headings}\n\n"
39
+ f"The section headings must appear exactly as written above: {names}. "
40
+ "Do not shorten or rename them, and do not merge a section with "
41
+ "another section."
42
+ )
43
+
44
+
45
+ def render_brief_prompt(sections: list[str]) -> str:
46
+ """The full brief prompt: static prose plus the section-rendered OUTPUT."""
47
+ return load_template() + "\n\n" + _output_block(sections)
@@ -25,12 +25,16 @@ from .secrets import scan_env_files
25
25
 
26
26
  def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
27
27
  """Every `agent:` step must resolve to an existing file under agents_dir
28
- (the same fail-fast `load_agent` enforces at run time)."""
28
+ (the same fail-fast `load_agent` enforces at run time).
29
+
30
+ The brief prompt is engine-embedded (046), so a ``brief_inputs:`` step is
31
+ exempt from the agents_dir lookup - it never reads a vendored file."""
29
32
  for step in cfg.steps:
30
- if step.agent is not None:
31
- path = agents_dir / f"{step.agent}.agent.md"
32
- if not path.is_file():
33
- raise FileNotFoundError(f"Agent file not found: {path}")
33
+ if step.agent is None or step.brief_inputs is not None:
34
+ continue
35
+ path = agents_dir / f"{step.agent}.agent.md"
36
+ if not path.is_file():
37
+ raise FileNotFoundError(f"Agent file not found: {path}")
34
38
 
35
39
 
36
40
  def _validate_actions(cfg: PipelineConfig, registry: Registry) -> None:
@@ -48,13 +48,20 @@ if not getattr(_ORIGINAL_FORMAT_MESSAGES, "_agentic_dev_loop_reasoning_repair",
48
48
  LLM.format_messages_for_llm = _repair_reasoning_passback
49
49
 
50
50
 
51
- def build_llm(cfg: LlmConfig) -> LLM:
51
+ def build_llm(cfg: LlmConfig, reasoning_effort: str | None = None) -> LLM:
52
52
  api_key = os.environ.get(cfg.api_key_env)
53
53
  if not api_key:
54
54
  raise RuntimeError(f"{cfg.api_key_env} environment variable is required")
55
- return LLM(
56
- model=cfg.model,
57
- api_key=api_key,
58
- base_url=cfg.base_url,
59
- timeout=cfg.timeout, # policy: no LLM timeout unless explicitly set
60
- )
55
+ # 047 (D2/D7): an explicit per-step override wins, else the llm block's
56
+ # value applies, else the field is omitted entirely and the SDK default
57
+ # ("high") is in force. The reasoning_content shims above stay untouched.
58
+ effort = reasoning_effort if reasoning_effort is not None else cfg.reasoning_effort
59
+ kwargs = {
60
+ "model": cfg.model,
61
+ "api_key": api_key,
62
+ "base_url": cfg.base_url,
63
+ "timeout": cfg.timeout, # policy: no LLM timeout unless explicitly set
64
+ }
65
+ if effort is not None:
66
+ kwargs["reasoning_effort"] = effort
67
+ return LLM(**kwargs)
@@ -32,8 +32,9 @@ from langgraph.graph import END
32
32
  from openhands.sdk import Conversation
33
33
 
34
34
  from .agent_error_guide import guide_section
35
- from .agents import load_agent, register_sandbox_terminal
35
+ from .agents import load_agent, load_brief_agent, register_sandbox_terminal
36
36
  from .brief_coverage import extract_anchors, missing_anchors
37
+ from .brief_prompt import BRIEF_PROMPT_VERSION
37
38
  from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
38
39
  from agentic_dev_loop.core.paths import _has_wildcards
39
40
  from .deps import extension_message, parse_missing_dependencies, policy_injection
@@ -41,6 +42,7 @@ from .egress_policy import assemble_egress_policy
41
42
  from .forbidden import resolve_forbidden_rules
42
43
  from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
43
44
  resolve_sandbox_env)
45
+ from .llm import build_llm
44
46
  from .mounts import resolve_mount_set
45
47
  from .outputs import CapturedOutput
46
48
  from .secrets import redact_values, step_secret_env
@@ -173,7 +175,18 @@ class Runtime:
173
175
  if step is not None else None),
174
176
  forbidden_commands=forbidden_commands,
175
177
  allowlist=allowlist)
176
- agent = load_agent(self.agents_dir, agent_name, self.llm)
178
+ # 047 (D7): a step's reasoning_effort overrides the llm block's;
179
+ # self.llm already carries the block value (or the SDK default),
180
+ # so only build a fresh LLM when the step overrides it.
181
+ llm = self.llm
182
+ if step is not None and step.reasoning_effort is not None:
183
+ llm = build_llm(self.cfg.llm,
184
+ reasoning_effort=step.reasoning_effort)
185
+ if step is not None and step.brief_inputs is not None:
186
+ agent = load_brief_agent(
187
+ agent_name, step.brief_inputs.sections, llm)
188
+ else:
189
+ agent = load_agent(self.agents_dir, agent_name, llm)
177
190
  callbacks = []
178
191
  if finalize_controller is not None:
179
192
  callbacks.append(finalize_controller.on_event)
@@ -543,6 +556,14 @@ def brief_inputs_hash(runtime: Runtime, step: StepConfig,
543
556
  brief step, otherwise the existing digest is reused without an LLM call."""
544
557
  sections, env_facts = _brief_sections(runtime, step, docs, doc_sources)
545
558
  digest = hashlib.sha256()
559
+ # 046, D6: the embedded prompt version and the configured section headings
560
+ # join the hash, so an engine upgrade or a sections edit busts the cached
561
+ # digest and forces a fresh capture.
562
+ digest.update(BRIEF_PROMPT_VERSION.encode("utf-8"))
563
+ digest.update(b"\0")
564
+ for section in step.brief_inputs.sections:
565
+ digest.update(section.encode("utf-8"))
566
+ digest.update(b"\0")
546
567
  for key in sorted(sections):
547
568
  digest.update(key.encode("utf-8"))
548
569
  digest.update(b"\0")
@@ -751,6 +772,20 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
751
772
  return node
752
773
 
753
774
 
775
+ def _warn_vendored_brief(runtime: Runtime, step: StepConfig) -> None:
776
+ """Warn once per load when a vendored brief prompt exists (046, D5).
777
+
778
+ The embedded prompt is authoritative, so a leftover
779
+ ``<agents_dir>/<agent>.agent.md`` for a ``brief_inputs:`` step is ignored;
780
+ the warning names it without failing the run."""
781
+ vendored = runtime.agents_dir / f"{step.agent}.agent.md"
782
+ if vendored.is_file():
783
+ print(
784
+ f"[{runtime.cfg.name}] ignoring vendored brief prompt {vendored} - "
785
+ "the engine-embedded prompt is authoritative",
786
+ file=sys.stderr, flush=True)
787
+
788
+
754
789
  def make_brief_node(runtime: Runtime, step: StepConfig):
755
790
  """Node closure for a ``brief_inputs:`` agent step (033).
756
791
 
@@ -768,6 +803,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
768
803
  re-runs the agent with a ``## Coverage retry`` correction note. Exhausting
769
804
  the budget writes last-error.md and fails the run."""
770
805
  captures = step.artifact is not None or bool(step.produces)
806
+ _warn_vendored_brief(runtime, step)
771
807
 
772
808
  def node(state: dict) -> dict:
773
809
  attempt = state["attempt"]
@@ -780,6 +816,8 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
780
816
  anchors = _brief_anchors(runtime, step)
781
817
  env_facts = (list(step.brief_inputs.env_facts)
782
818
  if step.brief_inputs is not None else [])
819
+ sections = (list(step.brief_inputs.sections)
820
+ if step.brief_inputs is not None else [])
783
821
  except Exception:
784
822
  err = redact_values(traceback.format_exc(), runtime.secrets.values())
785
823
  print(err[-4000:], file=sys.stderr, flush=True)
@@ -793,7 +831,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
793
831
  reused = (existing is not None
794
832
  and state.get("brief_hashes", {}).get(step.name) == current_hash)
795
833
 
796
- if reused and not missing_anchors(existing, anchors, env_facts):
834
+ if reused and not missing_anchors(existing, anchors, env_facts, sections):
797
835
  print(f"[{subject}] {step.name} brief inputs unchanged - reusing.",
798
836
  flush=True)
799
837
  merged = {**state["step_verdicts"], step.name: "pass"}
@@ -802,7 +840,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
802
840
  coverage_note = None
803
841
  if reused:
804
842
  coverage_note = _coverage_note_text(
805
- missing_anchors(existing, anchors, env_facts))
843
+ missing_anchors(existing, anchors, env_facts, sections))
806
844
  message = build_brief_context(runtime, step, state["docs"],
807
845
  state.get("doc_sources"),
808
846
  coverage_note=coverage_note)
@@ -824,7 +862,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
824
862
  result.truncated, max_iterations)],
825
863
  "retry_target": _unclear_target(runtime, step)}
826
864
 
827
- missing = missing_anchors(result.response or "", anchors, env_facts)
865
+ missing = missing_anchors(result.response or "", anchors, env_facts, sections)
828
866
  if missing:
829
867
  prev_hash = state.get("brief_hashes", {}).get(step.name)
830
868
  retries = (0 if prev_hash != current_hash