agentic-dev-loop 1.0.0a48__tar.gz → 1.0.0a50__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.0a48/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a50}/PKG-INFO +15 -11
  2. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/README.md +14 -10
  3. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/config.py +62 -31
  5. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/agents.py +6 -0
  6. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/dryrun.py +10 -3
  7. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/graph.py +3 -4
  8. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/isolation.py +35 -16
  9. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/stages.py +9 -8
  10. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50/src/agentic_dev_loop.egg-info}/PKG-INFO +15 -11
  11. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_033_brief_step.py +29 -62
  12. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_041_brief_coverage.py +10 -10
  13. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_agent_error_guide.py +1 -1
  14. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_config.py +83 -7
  15. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_containers.py +5 -1
  16. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_e2e_fixtures.py +7 -8
  17. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_isolation.py +69 -7
  18. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_publish.py +35 -0
  19. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/LICENSE +0 -0
  20. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/setup.cfg +0 -0
  21. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/__init__.py +0 -0
  22. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/__init__.py +0 -0
  23. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/paths.py +0 -0
  24. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/results.py +0 -0
  25. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/verdict.py +0 -0
  26. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/__init__.py +0 -0
  27. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/actions.py +0 -0
  28. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  29. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  30. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  31. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  32. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  33. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  34. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/cli.py +0 -0
  35. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/containers.py +0 -0
  36. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/deps.py +0 -0
  37. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  38. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  39. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  40. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  41. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  42. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  43. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/llm.py +0 -0
  44. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/loaders.py +0 -0
  45. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/mounts.py +0 -0
  46. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/outputs.py +0 -0
  47. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/presets.py +0 -0
  48. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/proc.py +0 -0
  49. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/registry.py +0 -0
  50. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  51. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  52. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/secrets.py +0 -0
  53. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  54. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  55. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/py.typed +0 -0
  56. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/viz/__init__.py +0 -0
  57. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/viz/__main__.py +0 -0
  58. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/viz/server.py +0 -0
  59. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  60. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  61. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  62. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  63. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  64. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_028_declared_caches.py +0 -0
  65. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_029_merge_on_conflict.py +0 -0
  66. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_035_finalize_nudge.py +0 -0
  67. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  68. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_046_brief_prompt.py +0 -0
  69. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_actions.py +0 -0
  70. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_agents.py +0 -0
  71. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_aidlc.py +0 -0
  72. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_cli.py +0 -0
  73. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_deps.py +0 -0
  74. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_dry_run.py +0 -0
  75. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_egress_proxy.py +0 -0
  76. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_graph.py +0 -0
  77. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_hostboundary.py +0 -0
  78. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_hostboundary_integration.py +0 -0
  79. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_isolation_integration.py +0 -0
  80. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_isolation_pbt.py +0 -0
  81. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_llm.py +0 -0
  82. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_loaders.py +0 -0
  83. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_module_contract.py +0 -0
  84. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_outputs.py +0 -0
  85. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_outputs_pbt.py +0 -0
  86. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_proc.py +0 -0
  87. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_qa_prompt.py +0 -0
  88. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_registry.py +0 -0
  89. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_repo_ascii.py +0 -0
  90. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_results.py +0 -0
  91. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_sdk_compat.py +0 -0
  92. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_secrets.py +0 -0
  93. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_secrets_integration.py +0 -0
  94. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_secrets_pipeline.py +0 -0
  95. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_source_resolution.py +0 -0
  96. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_stages.py +0 -0
  97. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_step_workspace.py +0 -0
  98. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_sync_back.py +0 -0
  99. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_trigger_script.py +0 -0
  100. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_unit5_config.py +0 -0
  101. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_unit5_pbt.py +0 -0
  102. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_unit5_pure.py +0 -0
  103. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/tests/test_verdict.py +0 -0
  104. {agentic_dev_loop-1.0.0a48 → agentic_dev_loop-1.0.0a50}/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.0a48
3
+ Version: 1.0.0a50
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
@@ -169,7 +169,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
169
169
  registry-owned orphans whose owner pid is dead (never unregistered names).
170
170
  A SIGTERM to a running pipeline still closes the sandbox (the graph's
171
171
  `finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
172
- the startup reaper on the next run or by `containers --reap`. Pass
172
+ the startup reaper on the next run or by `containers --reap`. The reaper
173
+ and `--reap` prune a registry entry only after the daemon confirms the
174
+ object is gone; a surviving object (a failed removal) keeps its entry so
175
+ the next startup retries it. Pass
173
176
  `--state-dir` to point at a custom `isolation.state_dir`.
174
177
 
175
178
  **What a run looks like** (the shipped self-loop example):
@@ -206,10 +209,11 @@ agents_dir: sdk_agents # your <name>.agent.md f
206
209
 
207
210
  steps:
208
211
  - name: brief
209
- agent: brief
210
- produces: attempt-brief.md
211
- max_iterations: 12
212
- brief_inputs: { include: [AGENTS.md, coding-standards-ddd.yaml], env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"] }
212
+ reasoning_effort: medium
213
+ brief_inputs:
214
+ include: [AGENTS.md, coding-standards-ddd.yaml]
215
+ sections: [Repo rules digest, Unit scope digest, Environment facts, Open risks]
216
+ env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"]
213
217
  on_pass: dev
214
218
 
215
219
  - name: dev
@@ -249,9 +253,9 @@ steps:
249
253
 
250
254
  | Key | Notes |
251
255
  | --- | --- |
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)). |
256
+ | `agent:` / `command:` / `action:` / `brief_inputs:` | 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)). `brief_inputs:` is a native brief step - see its row below. |
253
257
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
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. |
258
+ | `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the 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). `name` stays free-form, so multiple brief steps are allowed. |
255
259
  | `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. |
256
260
  | `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). |
257
261
  | `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. |
@@ -294,9 +298,9 @@ Recommended lane defaults (guidance only, not enforced):
294
298
  | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
295
299
 
296
300
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
297
- run start (or when its inputs change), its `produces:` artifact survives attempt
298
- starts, and the dev/review agents read that digest instead of re-deriving repo
299
- rules and environment facts every attempt.
301
+ run start (or when its inputs change), its engine-owned `attempt-brief.md`
302
+ digest survives attempt starts, and the dev/review agents read that digest
303
+ instead of re-deriving repo rules and environment facts every attempt.
300
304
 
301
305
  The brief digest is mechanically coverage-checked: the configured
302
306
  `brief_inputs.sections` headings must each appear on their own line, every
@@ -125,7 +125,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
125
125
  registry-owned orphans whose owner pid is dead (never unregistered names).
126
126
  A SIGTERM to a running pipeline still closes the sandbox (the graph's
127
127
  `finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
128
- the startup reaper on the next run or by `containers --reap`. Pass
128
+ the startup reaper on the next run or by `containers --reap`. The reaper
129
+ and `--reap` prune a registry entry only after the daemon confirms the
130
+ object is gone; a surviving object (a failed removal) keeps its entry so
131
+ the next startup retries it. Pass
129
132
  `--state-dir` to point at a custom `isolation.state_dir`.
130
133
 
131
134
  **What a run looks like** (the shipped self-loop example):
@@ -162,10 +165,11 @@ agents_dir: sdk_agents # your <name>.agent.md f
162
165
 
163
166
  steps:
164
167
  - name: brief
165
- agent: brief
166
- produces: attempt-brief.md
167
- max_iterations: 12
168
- brief_inputs: { include: [AGENTS.md, coding-standards-ddd.yaml], env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"] }
168
+ reasoning_effort: medium
169
+ brief_inputs:
170
+ include: [AGENTS.md, coding-standards-ddd.yaml]
171
+ sections: [Repo rules digest, Unit scope digest, Environment facts, Open risks]
172
+ env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"]
169
173
  on_pass: dev
170
174
 
171
175
  - name: dev
@@ -205,9 +209,9 @@ steps:
205
209
 
206
210
  | Key | Notes |
207
211
  | --- | --- |
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)). |
212
+ | `agent:` / `command:` / `action:` / `brief_inputs:` | 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)). `brief_inputs:` is a native brief step - see its row below. |
209
213
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
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. |
214
+ | `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the 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). `name` stays free-form, so multiple brief steps are allowed. |
211
215
  | `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. |
212
216
  | `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). |
213
217
  | `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. |
@@ -250,9 +254,9 @@ Recommended lane defaults (guidance only, not enforced):
250
254
  | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
251
255
 
252
256
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
253
- run start (or when its inputs change), its `produces:` artifact survives attempt
254
- starts, and the dev/review agents read that digest instead of re-deriving repo
255
- rules and environment facts every attempt.
257
+ run start (or when its inputs change), its engine-owned `attempt-brief.md`
258
+ digest survives attempt starts, and the dev/review agents read that digest
259
+ instead of re-deriving repo rules and environment facts every attempt.
256
260
 
257
261
  The brief digest is mechanically coverage-checked: the configured
258
262
  `brief_inputs.sections` headings must each appear on their own line, every
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a48"
7
+ version = "1.0.0a50"
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"
@@ -35,6 +35,13 @@ DEFAULT_BRIEF_SECTIONS = [
35
35
  "Open risks",
36
36
  ]
37
37
 
38
+ # 049: a brief step is engine-owned end to end - the capture file and the
39
+ # iteration cap are fixed, not configurable. Downstream consumers (capture
40
+ # write, hygiene, context, retry classification) keep reading `produces` and
41
+ # `max_iterations`; parse-time normalization applies these after validation.
42
+ BRIEF_PRODUCES = ["attempt-brief.md"]
43
+ BRIEF_MAX_ITERATIONS = 12
44
+
38
45
  # 047: the full SDK reasoning_effort literal, passed through verbatim. The
39
46
  # engine is provider-generic, so it validates shape only - whether a given
40
47
  # provider accepts every value (notably "none") is operator risk (D3).
@@ -372,9 +379,9 @@ class BriefInputsSpec(BaseModel):
372
379
 
373
380
  class StepConfig(BaseModel):
374
381
  """One pipeline step. Its type IS the key it carries: exactly one of
375
- `agent`, `action`, or `command` - empty values do not count, anything
376
- else fails to load. See the `kind` property (the single discriminator
377
- graph-building dispatches on)."""
382
+ `agent`, `action`, `command`, or `brief_inputs` - empty values do not
383
+ count, anything else fails to load. See the `kind` property (the single
384
+ discriminator graph-building dispatches on)."""
378
385
  model_config = ConfigDict(extra="forbid")
379
386
 
380
387
  name: str
@@ -413,10 +420,12 @@ class StepConfig(BaseModel):
413
420
 
414
421
  @property
415
422
  def kind(self) -> str:
416
- """The step's type: 'agent', 'command', or 'action'. Which of the
417
- three keys is set is the ONLY signal; validation has already
423
+ """The step's type: 'brief', 'agent', 'command', or 'action'. Which
424
+ of the four keys is set is the ONLY signal; validation has already
418
425
  guaranteed exactly one is set and non-empty by the time any instance
419
426
  exists."""
427
+ if self.brief_inputs is not None:
428
+ return "brief"
420
429
  if self.agent is not None:
421
430
  return "agent"
422
431
  if self.command is not None:
@@ -470,11 +479,48 @@ class StepConfig(BaseModel):
470
479
  raise ValueError(
471
480
  f"step {self.name!r}: {key!r} must be a non-empty string"
472
481
  )
473
- if sum(k is not None for k in (self.agent, self.action, self.command)) != 1:
482
+ if sum(k is not None for k in
483
+ (self.agent, self.action, self.command, self.brief_inputs)) != 1:
474
484
  raise ValueError(
475
- f"step {self.name!r}: exactly one of 'agent', 'action', or "
476
- "'command' is required"
485
+ f"step {self.name!r}: exactly one of 'agent', 'action', "
486
+ "'command', or 'brief_inputs' is required"
477
487
  )
488
+ if self.brief_inputs is not None:
489
+ if self.produces:
490
+ raise ValueError(
491
+ f"step {self.name!r}: 'brief_inputs' steps write "
492
+ f"{BRIEF_PRODUCES[0]!r}; drop 'produces'")
493
+ if self.artifact is not None:
494
+ raise ValueError(
495
+ f"step {self.name!r}: 'brief_inputs' steps have no "
496
+ "verdicts; drop 'artifact'")
497
+ if self.verdicts is not None:
498
+ raise ValueError(
499
+ f"step {self.name!r}: 'brief_inputs' steps have no "
500
+ "verdicts; drop 'verdicts'")
501
+ if self.params:
502
+ raise ValueError(
503
+ f"step {self.name!r}: 'brief_inputs' steps take no "
504
+ "'params'")
505
+ if self.max_iterations is not None:
506
+ raise ValueError(
507
+ f"step {self.name!r}: 'brief_inputs' steps use the "
508
+ f"engine cap ({BRIEF_MAX_ITERATIONS}); drop "
509
+ "'max_iterations'")
510
+ if self.timeout is not None:
511
+ raise ValueError(
512
+ f"step {self.name!r}: 'timeout' is command-only; drop it "
513
+ "from a 'brief_inputs' step")
514
+ if self.custom_prompt is not None:
515
+ raise ValueError(
516
+ f"step {self.name!r}: the brief prompt is engine-owned; "
517
+ "drop 'custom_prompt'")
518
+ if self.finalize_nudge_turns is not None:
519
+ raise ValueError(
520
+ f"step {self.name!r}: 'brief_inputs' steps have no "
521
+ "verdicts; drop 'finalize_nudge_turns'")
522
+ self.produces = list(BRIEF_PRODUCES)
523
+ self.max_iterations = BRIEF_MAX_ITERATIONS
478
524
  if self.command is not None:
479
525
  for field, empty in (("artifact", None), ("verdicts", None),
480
526
  ("produces", []), ("params", {}),
@@ -495,9 +541,11 @@ class StepConfig(BaseModel):
495
541
  raise ValueError(
496
542
  f"step {self.name!r}: 'timeout' requires 'command'"
497
543
  )
498
- if self.terminal_timeout is not None and self.agent is None:
544
+ if (self.terminal_timeout is not None
545
+ and self.agent is None and self.brief_inputs is None):
499
546
  raise ValueError(
500
- f"step {self.name!r}: 'terminal_timeout' requires 'agent'"
547
+ f"step {self.name!r}: 'terminal_timeout' requires 'agent' or "
548
+ "'brief_inputs'"
501
549
  )
502
550
  if self.finalize_nudge_turns is not None and self.agent is None:
503
551
  raise ValueError(
@@ -514,37 +562,20 @@ class StepConfig(BaseModel):
514
562
  f"step {self.name!r}: 'finalize_nudge_turns' must be less "
515
563
  "than 'max_iterations'"
516
564
  )
517
- if self.brief_inputs is not None and self.agent is None:
518
- raise ValueError(
519
- f"step {self.name!r}: 'brief_inputs' requires 'agent'"
520
- )
521
565
  if self.custom_prompt is not None and self.agent is None:
522
566
  raise ValueError(
523
567
  f"step {self.name!r}: 'custom_prompt' requires 'agent'"
524
568
  )
525
- if (self.brief_inputs is not None
526
- and (self.artifact is not None or self.verdicts is not None)):
527
- raise ValueError(
528
- f"step {self.name!r}: 'brief_inputs' is only valid on a "
529
- "produces-only agent step"
530
- )
531
- if self.brief_inputs is not None and not self.produces:
532
- raise ValueError(
533
- f"step {self.name!r}: 'brief_inputs' requires 'produces'"
534
- )
535
- if self.setup is not None and self.command is None and self.agent is None:
569
+ if (self.setup is not None and self.command is None
570
+ and self.agent is None and self.brief_inputs is None):
536
571
  raise ValueError(
537
- f"step {self.name!r}: 'setup' is only allowed on 'command' or 'agent' steps"
572
+ f"step {self.name!r}: 'setup' is only allowed on 'command', "
573
+ "'agent', or 'brief_inputs' steps"
538
574
  )
539
575
  if self.setup_probe is not None and self.setup is None:
540
576
  raise ValueError(
541
577
  f"step {self.name!r}: 'setup_probe' requires 'setup'"
542
578
  )
543
- # `network` is NOT rejected here: the schema can't tell a host-side
544
- # built-in action (e.g. `commit`, which must never take egress) from
545
- # a sandbox-executed consumer action (which may, like a command step).
546
- # That distinction is enforced once the registry is known - see
547
- # `validate_action_network` (020).
548
579
  if self.secrets and self.action is not None:
549
580
  raise ValueError(
550
581
  f"step {self.name!r}: 'secrets' is only allowed on 'agent' or "
@@ -25,6 +25,12 @@ if TYPE_CHECKING:
25
25
 
26
26
  from .isolation import Sandbox
27
27
 
28
+ # 049, D3: the internal brief agent name is a fixed UUID4 string, never the
29
+ # literal "brief", so a consumer vendoring a `brief.agent.md` cannot mint a
30
+ # second AgentDefinition under the same name. The vendored-file warning still
31
+ # keys on the literal filename `brief.agent.md` (that is the file to delete).
32
+ BRIEF_AGENT_NAME = "4f1c9e2b-7a3d-4e5f-8b6c-0d1a2f3c4d5e"
33
+
28
34
 
29
35
  def load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
30
36
  """Load `<agents_dir>/<name>.agent.md` and build the Agent via the SDK
@@ -27,10 +27,11 @@ def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
27
27
  """Every `agent:` step must resolve to an existing file under agents_dir
28
28
  (the same fail-fast `load_agent` enforces at run time).
29
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."""
30
+ The brief prompt is engine-embedded (046), so a native ``brief_inputs:``
31
+ step (kind 'brief') has `agent is None` and is exempt from the agents_dir
32
+ lookup - it never reads a vendored file."""
32
33
  for step in cfg.steps:
33
- if step.agent is None or step.brief_inputs is not None:
34
+ if step.agent is None:
34
35
  continue
35
36
  path = agents_dir / f"{step.agent}.agent.md"
36
37
  if not path.is_file():
@@ -79,6 +80,12 @@ def _step_summary(step: StepConfig) -> str:
79
80
  parts.append(f"produces={','.join(step.produces)}")
80
81
  if step.max_iterations is not None:
81
82
  parts.append(f"max_iterations={step.max_iterations}")
83
+ elif step.kind == "brief":
84
+ parts = [f"sections={len(step.brief_inputs.sections)}"]
85
+ if step.produces:
86
+ parts.append(f"produces={','.join(step.produces)}")
87
+ if step.max_iterations is not None:
88
+ parts.append(f"max_iterations={step.max_iterations}")
82
89
  elif step.kind == "command":
83
90
  parts = [f"command={step.command}"]
84
91
  if step.timeout is not None:
@@ -93,10 +93,9 @@ def build_graph(runtime: Runtime, checkpointer=None):
93
93
  graph = StateGraph(LoopState)
94
94
  for step in runtime.cfg.steps:
95
95
  if step.kind == "agent":
96
- if step.brief_inputs is not None:
97
- node = make_brief_node(runtime, step)
98
- else:
99
- node = make_agent_node(runtime, step)
96
+ node = make_agent_node(runtime, step)
97
+ elif step.kind == "brief":
98
+ node = make_brief_node(runtime, step)
100
99
  elif step.kind == "command":
101
100
  node = make_command_node(runtime, step)
102
101
  else:
@@ -1632,23 +1632,12 @@ class DockerBackend(Sandbox):
1632
1632
  _write_registry(self._iso_state_dir, remaining)
1633
1633
 
1634
1634
  def _container_gone(self, name: str) -> bool:
1635
- """True only when the daemon confirms the container no longer exists.
1636
- A nonzero daemon exit keeps the entry."""
1637
- res = self._runner.run(
1638
- ["docker", "ps", "-a", "--filter", f"name={name}",
1639
- "--format", "{{.Names}}"], timeout=30)
1640
- if res.exit_code != 0:
1641
- return False
1642
- return name not in res.stdout.splitlines()
1635
+ """Thin delegation to the module-level confirm helper (050 D2)."""
1636
+ return _container_gone(self._runner, name)
1643
1637
 
1644
1638
  def _network_gone(self, name: str) -> bool:
1645
- """True only when the daemon reports the network is missing. Any
1646
- other inspect failure (daemon unreachable) keeps the entry."""
1647
- res = self._runner.run(
1648
- ["docker", "network", "inspect", name], timeout=30)
1649
- if res.exit_code == 0:
1650
- return False
1651
- return "No such network" in f"{res.stderr} {res.stdout}"
1639
+ """Thin delegation to the module-level confirm helper (050 D2)."""
1640
+ return _network_gone(self._runner, name)
1652
1641
 
1653
1642
  # -- internal -----------------------------------------------------------
1654
1643
 
@@ -1882,6 +1871,28 @@ def _pid_alive_matches(pid: int, recorded_start: str | None) -> bool:
1882
1871
  return current == recorded_start
1883
1872
 
1884
1873
 
1874
+ def _container_gone(runner: CommandRunner, name: str) -> bool:
1875
+ """True only when the daemon confirms the container no longer exists.
1876
+ A nonzero daemon exit keeps the entry."""
1877
+ res = runner.run(
1878
+ ["docker", "ps", "-a", "--filter", f"name={name}",
1879
+ "--format", "{{.Names}}"], timeout=30)
1880
+ if res.exit_code != 0:
1881
+ return False
1882
+ return name not in res.stdout.splitlines()
1883
+
1884
+
1885
+ def _network_gone(runner: CommandRunner, name: str) -> bool:
1886
+ """True only when the daemon reports the network is missing. Any
1887
+ other inspect failure (daemon unreachable) keeps the entry."""
1888
+ res = runner.run(
1889
+ ["docker", "network", "inspect", name], timeout=30)
1890
+ if res.exit_code == 0:
1891
+ return False
1892
+ return "No such network" in f"{res.stderr} {res.stdout}"
1893
+
1894
+
1895
+
1885
1896
  def reap_orphaned_containers(state_dir: str | Path,
1886
1897
  runner: CommandRunner | None = None,
1887
1898
  *, announce: bool = True) -> list[str]:
@@ -1889,7 +1900,9 @@ def reap_orphaned_containers(state_dir: str | Path,
1889
1900
  and the per-run internal network whose owning CLI process is gone (crashed
1890
1901
  before close()). Exact by construction - only engine registry names are
1891
1902
  touched, and a live owner (pid alive with the same start time) is left
1892
- alone. Best-effort: never raises."""
1903
+ alone. A surviving object (rm failure) keeps its entry so the next
1904
+ startup reaper retries; only a daemon-confirmed absence prunes it (050
1905
+ D1). Best-effort: never raises."""
1893
1906
  runner = runner or _default_runner
1894
1907
  entries = _read_registry(Path(state_dir))
1895
1908
  if not entries:
@@ -1909,6 +1922,9 @@ def reap_orphaned_containers(state_dir: str | Path,
1909
1922
  remaining.append(entry)
1910
1923
  continue
1911
1924
  runner.run(["docker", "network", "rm", name], timeout=60)
1925
+ if not _network_gone(runner, name):
1926
+ remaining.append(entry)
1927
+ continue
1912
1928
  if announce:
1913
1929
  _announce(f"reaped orphaned sandbox network {name}")
1914
1930
  reaped.append(name)
@@ -1924,6 +1940,9 @@ def reap_orphaned_containers(state_dir: str | Path,
1924
1940
  remaining.append(entry)
1925
1941
  continue
1926
1942
  runner.run(["docker", "rm", "-f", name], timeout=60)
1943
+ if not _container_gone(runner, name):
1944
+ remaining.append(entry)
1945
+ continue
1927
1946
  if announce:
1928
1947
  _announce(f"reaped orphaned sandbox container {name}")
1929
1948
  reaped.append(name)
@@ -32,7 +32,8 @@ 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, load_brief_agent, register_sandbox_terminal
35
+ from .agents import (BRIEF_AGENT_NAME, load_agent, load_brief_agent,
36
+ register_sandbox_terminal)
36
37
  from .brief_coverage import extract_anchors, missing_anchors
37
38
  from .brief_prompt import BRIEF_PROMPT_VERSION
38
39
  from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
@@ -182,7 +183,7 @@ class Runtime:
182
183
  if step is not None and step.reasoning_effort is not None:
183
184
  llm = build_llm(self.cfg.llm,
184
185
  reasoning_effort=step.reasoning_effort)
185
- if step is not None and step.brief_inputs is not None:
186
+ if step is not None and step.kind == "brief":
186
187
  agent = load_brief_agent(
187
188
  agent_name, step.brief_inputs.sections, llm)
188
189
  else:
@@ -398,7 +399,7 @@ def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
398
399
  parts.append("## Dependency policy\n\n" + policy_injection(runtime.cfg))
399
400
  lines = []
400
401
  for step in runtime.cfg.steps:
401
- if step.agent is None:
402
+ if step.kind not in ("agent", "brief"):
402
403
  continue
403
404
  for produced in step.produces:
404
405
  lines.append(f"{step.name} agent writes: `{prompt_dir(runtime)}/{produced}`")
@@ -428,7 +429,7 @@ def run_scoped_artifacts(cfg: PipelineConfig) -> list[str]:
428
429
  outputs are exempt from per-attempt hygiene."""
429
430
  files = []
430
431
  for step in cfg.steps:
431
- if step.brief_inputs is None:
432
+ if step.kind != "brief":
432
433
  continue
433
434
  files.extend(step.produces)
434
435
  if step.artifact is not None:
@@ -778,7 +779,7 @@ def _warn_vendored_brief(runtime: Runtime, step: StepConfig) -> None:
778
779
  The embedded prompt is authoritative, so a leftover
779
780
  ``<agents_dir>/<agent>.agent.md`` for a ``brief_inputs:`` step is ignored;
780
781
  the warning names it without failing the run."""
781
- vendored = runtime.agents_dir / f"{step.agent}.agent.md"
782
+ vendored = runtime.agents_dir / "brief.agent.md"
782
783
  if vendored.is_file():
783
784
  print(
784
785
  f"[{runtime.cfg.name}] ignoring vendored brief prompt {vendored} - "
@@ -845,8 +846,8 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
845
846
  state.get("doc_sources"),
846
847
  coverage_note=coverage_note)
847
848
 
848
- print(f"[{subject}] Running {step.agent} agent...", flush=True)
849
- result = runtime.run_agent(step.agent, message,
849
+ print(f"[{subject}] Running {step.name} agent...", flush=True)
850
+ result = runtime.run_agent(BRIEF_AGENT_NAME, message,
850
851
  max_iterations=max_iterations, step=step)
851
852
  if not result.ok:
852
853
  print(f"[{subject}] {step.name} agent failed on attempt {attempt}.",
@@ -1125,7 +1126,7 @@ def make_bump_node(runtime: Runtime):
1125
1126
  # re-enter that brief step so it regenerates once and refreshes its
1126
1127
  # hash. Unchanged inputs keep the failing step's retry_target.
1127
1128
  for step in runtime.cfg.steps:
1128
- if step.brief_inputs is None:
1129
+ if step.kind != "brief":
1129
1130
  continue
1130
1131
  try:
1131
1132
  current = brief_inputs_hash(runtime, step,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a48
3
+ Version: 1.0.0a50
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
@@ -169,7 +169,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
169
169
  registry-owned orphans whose owner pid is dead (never unregistered names).
170
170
  A SIGTERM to a running pipeline still closes the sandbox (the graph's
171
171
  `finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
172
- the startup reaper on the next run or by `containers --reap`. Pass
172
+ the startup reaper on the next run or by `containers --reap`. The reaper
173
+ and `--reap` prune a registry entry only after the daemon confirms the
174
+ object is gone; a surviving object (a failed removal) keeps its entry so
175
+ the next startup retries it. Pass
173
176
  `--state-dir` to point at a custom `isolation.state_dir`.
174
177
 
175
178
  **What a run looks like** (the shipped self-loop example):
@@ -206,10 +209,11 @@ agents_dir: sdk_agents # your <name>.agent.md f
206
209
 
207
210
  steps:
208
211
  - name: brief
209
- agent: brief
210
- produces: attempt-brief.md
211
- max_iterations: 12
212
- brief_inputs: { include: [AGENTS.md, coding-standards-ddd.yaml], env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"] }
212
+ reasoning_effort: medium
213
+ brief_inputs:
214
+ include: [AGENTS.md, coding-standards-ddd.yaml]
215
+ sections: [Repo rules digest, Unit scope digest, Environment facts, Open risks]
216
+ env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"]
213
217
  on_pass: dev
214
218
 
215
219
  - name: dev
@@ -249,9 +253,9 @@ steps:
249
253
 
250
254
  | Key | Notes |
251
255
  | --- | --- |
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)). |
256
+ | `agent:` / `command:` / `action:` / `brief_inputs:` | 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)). `brief_inputs:` is a native brief step - see its row below. |
253
257
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
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. |
258
+ | `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the 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). `name` stays free-form, so multiple brief steps are allowed. |
255
259
  | `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. |
256
260
  | `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). |
257
261
  | `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. |
@@ -294,9 +298,9 @@ Recommended lane defaults (guidance only, not enforced):
294
298
  | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
295
299
 
296
300
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
297
- run start (or when its inputs change), its `produces:` artifact survives attempt
298
- starts, and the dev/review agents read that digest instead of re-deriving repo
299
- rules and environment facts every attempt.
301
+ run start (or when its inputs change), its engine-owned `attempt-brief.md`
302
+ digest survives attempt starts, and the dev/review agents read that digest
303
+ instead of re-deriving repo rules and environment facts every attempt.
300
304
 
301
305
  The brief digest is mechanically coverage-checked: the configured
302
306
  `brief_inputs.sections` headings must each appear on their own line, every