agentic-dev-loop 1.0.0a29__tar.gz → 1.0.0a31__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 (94) hide show
  1. {agentic_dev_loop-1.0.0a29/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a31}/PKG-INFO +36 -1
  2. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/README.md +35 -0
  3. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/pyproject.toml +2 -1
  4. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/config.py +48 -0
  5. agentic_dev_loop-1.0.0a31/src/agentic_dev_loop/engine/agent_error_guide.md +6 -0
  6. agentic_dev_loop-1.0.0a31/src/agentic_dev_loop/engine/agent_error_guide.py +26 -0
  7. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/dryrun.py +4 -1
  8. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/extensions/aidlc.py +23 -1
  9. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/graph.py +13 -5
  10. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/loaders.py +18 -3
  11. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/stages.py +235 -11
  12. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31/src/agentic_dev_loop.egg-info}/PKG-INFO +36 -1
  13. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/SOURCES.txt +4 -0
  14. agentic_dev_loop-1.0.0a31/tests/test_033_brief_step.py +573 -0
  15. agentic_dev_loop-1.0.0a31/tests/test_agent_error_guide.py +184 -0
  16. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_e2e_fixtures.py +10 -4
  17. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_viz.py +1 -1
  18. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/LICENSE +0 -0
  19. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/setup.cfg +0 -0
  20. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/__init__.py +0 -0
  21. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/__init__.py +0 -0
  22. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/paths.py +0 -0
  23. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/results.py +0 -0
  24. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/verdict.py +0 -0
  25. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/__init__.py +0 -0
  26. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/actions.py +0 -0
  27. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/agents.py +0 -0
  28. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/cli.py +0 -0
  29. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/deps.py +0 -0
  30. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  31. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  32. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  33. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  34. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/isolation.py +0 -0
  35. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/llm.py +0 -0
  36. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/mounts.py +0 -0
  37. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/outputs.py +0 -0
  38. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/presets.py +0 -0
  39. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/proc.py +0 -0
  40. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/registry.py +0 -0
  41. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  42. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  43. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/secrets.py +0 -0
  44. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  45. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  46. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/py.typed +0 -0
  47. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/viz/__init__.py +0 -0
  48. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/viz/__main__.py +0 -0
  49. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/viz/server.py +0 -0
  50. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  51. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  52. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  53. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  54. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_028_declared_caches.py +0 -0
  55. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_029_merge_on_conflict.py +0 -0
  56. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_032_stop_when_green.py +0 -0
  57. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_actions.py +0 -0
  58. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_agents.py +0 -0
  59. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_aidlc.py +0 -0
  60. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_cli.py +0 -0
  61. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_config.py +0 -0
  62. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_deps.py +0 -0
  63. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_dry_run.py +0 -0
  64. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_egress_proxy.py +0 -0
  65. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_graph.py +0 -0
  66. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_hostboundary.py +0 -0
  67. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_hostboundary_integration.py +0 -0
  68. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_isolation.py +0 -0
  69. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_isolation_integration.py +0 -0
  70. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_isolation_pbt.py +0 -0
  71. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_llm.py +0 -0
  72. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_loaders.py +0 -0
  73. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_module_contract.py +0 -0
  74. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_outputs.py +0 -0
  75. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_outputs_pbt.py +0 -0
  76. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_proc.py +0 -0
  77. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_publish.py +0 -0
  78. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_qa_prompt.py +0 -0
  79. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_registry.py +0 -0
  80. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_repo_ascii.py +0 -0
  81. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_results.py +0 -0
  82. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_sdk_compat.py +0 -0
  83. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_secrets.py +0 -0
  84. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_secrets_integration.py +0 -0
  85. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_secrets_pipeline.py +0 -0
  86. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_source_resolution.py +0 -0
  87. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_stages.py +0 -0
  88. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_step_workspace.py +0 -0
  89. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_sync_back.py +0 -0
  90. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_trigger_script.py +0 -0
  91. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_unit5_config.py +0 -0
  92. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_unit5_pbt.py +0 -0
  93. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_unit5_pure.py +0 -0
  94. {agentic_dev_loop-1.0.0a29 → agentic_dev_loop-1.0.0a31}/tests/test_verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a29
3
+ Version: 1.0.0a31
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
@@ -192,6 +192,13 @@ max_attempts: 10 # CLI --max-attempts ove
192
192
  agents_dir: sdk_agents # your <name>.agent.md files
193
193
 
194
194
  steps:
195
+ - name: brief
196
+ agent: brief
197
+ produces: attempt-brief.md
198
+ max_iterations: 12
199
+ 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"] }
200
+ on_pass: dev
201
+
195
202
  - name: dev
196
203
  agent: dev
197
204
  produces: implementation-summary.md
@@ -229,6 +236,7 @@ steps:
229
236
  | --- | --- |
230
237
  | `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)). |
231
238
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
239
+ | `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. |
232
240
  | `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
233
241
  | `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. |
234
242
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
@@ -243,6 +251,11 @@ steps:
243
251
  | `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). |
244
252
  | `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). |
245
253
 
254
+ A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
255
+ run start (or when its inputs change), its `produces:` artifact survives attempt
256
+ starts, and the dev/review agents read that digest instead of re-deriving repo
257
+ rules and environment facts every attempt.
258
+
246
259
  ### Secrets (`secrets:`) - refs only, never values
247
260
 
248
261
  Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
@@ -528,6 +541,28 @@ dotnet restore --locked-mode # installs exactly packages.lock.json
528
541
  Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
529
542
  to prove it holds from a clean cache before setting `deps_policy: locked`.
530
543
 
544
+ ## Agent-facing error guide
545
+
546
+ The engine bundles an always-on guide for the engine error codes an agent
547
+ can actually observe while running terminal commands. The guide lives at
548
+ `src/agentic_dev_loop/engine/agent_error_guide.md`, versioned with the engine
549
+ code that emits the codes, and is injected into every agent step's task
550
+ context at assembly time. There is no opt-in and no opt-out in v1; command
551
+ and action steps never receive it because no agent reads them.
552
+
553
+ Each agent-visible code has exactly one table row, shaped like this:
554
+
555
+ | Code | Meaning | Correct response |
556
+ | --- | --- | --- |
557
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
558
+
559
+ The guide is consumer-agnostic: it explains the general mechanism and never
560
+ names a consumer or a consumer domain rule ID. If an ID must appear inside a
561
+ quoted engine message, the placeholder `<domain-rule-id>` is used. Any engine
562
+ change that adds or changes an agent-visible error code must update the guide
563
+ in the same commit; the test suite enforces the pairing by parsing the
564
+ agent-visible `SandboxError` raise sites against the bundled table.
565
+
531
566
  ## Host boundary (security model)
532
567
 
533
568
  The orchestrator, tool layer, and the `commit` action run in the **host
@@ -150,6 +150,13 @@ max_attempts: 10 # CLI --max-attempts ove
150
150
  agents_dir: sdk_agents # your <name>.agent.md files
151
151
 
152
152
  steps:
153
+ - name: brief
154
+ agent: brief
155
+ produces: attempt-brief.md
156
+ max_iterations: 12
157
+ 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"] }
158
+ on_pass: dev
159
+
153
160
  - name: dev
154
161
  agent: dev
155
162
  produces: implementation-summary.md
@@ -187,6 +194,7 @@ steps:
187
194
  | --- | --- |
188
195
  | `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)). |
189
196
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
197
+ | `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. |
190
198
  | `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
191
199
  | `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. |
192
200
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
@@ -201,6 +209,11 @@ steps:
201
209
  | `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). |
202
210
  | `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). |
203
211
 
212
+ A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
213
+ run start (or when its inputs change), its `produces:` artifact survives attempt
214
+ starts, and the dev/review agents read that digest instead of re-deriving repo
215
+ rules and environment facts every attempt.
216
+
204
217
  ### Secrets (`secrets:`) - refs only, never values
205
218
 
206
219
  Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
@@ -486,6 +499,28 @@ dotnet restore --locked-mode # installs exactly packages.lock.json
486
499
  Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
487
500
  to prove it holds from a clean cache before setting `deps_policy: locked`.
488
501
 
502
+ ## Agent-facing error guide
503
+
504
+ The engine bundles an always-on guide for the engine error codes an agent
505
+ can actually observe while running terminal commands. The guide lives at
506
+ `src/agentic_dev_loop/engine/agent_error_guide.md`, versioned with the engine
507
+ code that emits the codes, and is injected into every agent step's task
508
+ context at assembly time. There is no opt-in and no opt-out in v1; command
509
+ and action steps never receive it because no agent reads them.
510
+
511
+ Each agent-visible code has exactly one table row, shaped like this:
512
+
513
+ | Code | Meaning | Correct response |
514
+ | --- | --- | --- |
515
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
516
+
517
+ The guide is consumer-agnostic: it explains the general mechanism and never
518
+ names a consumer or a consumer domain rule ID. If an ID must appear inside a
519
+ quoted engine message, the placeholder `<domain-rule-id>` is used. Any engine
520
+ change that adds or changes an agent-visible error code must update the guide
521
+ in the same commit; the test suite enforces the pairing by parsing the
522
+ agent-visible `SandboxError` raise sites against the bundled table.
523
+
489
524
  ## Host boundary (security model)
490
525
 
491
526
  The orchestrator, tool layer, and the `commit` action run in the **host
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a29"
7
+ version = "1.0.0a31"
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"
@@ -68,6 +68,7 @@ where = ["src"]
68
68
 
69
69
  [tool.setuptools.package-data]
70
70
  agentic_dev_loop = ["py.typed"]
71
+ "agentic_dev_loop.engine" = ["*.md"]
71
72
  "agentic_dev_loop.engine.seccomp" = ["*.json"]
72
73
 
73
74
  [tool.pytest.ini_options]
@@ -335,6 +335,39 @@ class StopWhenSpec(BaseModel):
335
335
  return v
336
336
 
337
337
 
338
+ class BriefInputsSpec(BaseModel):
339
+ """Optional ``brief_inputs:`` on an agent step (033): the closed input
340
+ list a brief agent digests once per run. ``include`` is repo-relative
341
+ paths/globs (wildcards allowed); ``env_facts`` are the stable environment
342
+ facts handed to the agent verbatim so it never discovers them by running
343
+ commands. Both accept a single string or a list; unknown keys fail at
344
+ load."""
345
+
346
+ model_config = ConfigDict(extra="forbid")
347
+
348
+ include: list[str] = []
349
+ env_facts: list[str] = []
350
+
351
+ @field_validator("include", "env_facts", mode="before")
352
+ @classmethod
353
+ def _coerce_list(cls, v: Any) -> Any:
354
+ return [v] if isinstance(v, str) else v
355
+
356
+ @field_validator("include")
357
+ @classmethod
358
+ def _valid_include(cls, v: list[str]) -> list[str]:
359
+ return [_validate_declared_path(p, allow_wildcards=True) for p in v]
360
+
361
+ @field_validator("env_facts")
362
+ @classmethod
363
+ def _valid_env_facts(cls, v: list[str]) -> list[str]:
364
+ for fact in v:
365
+ if not isinstance(fact, str) or not fact.strip():
366
+ raise ValueError(
367
+ "brief_inputs.env_facts entries must be non-empty strings")
368
+ return v
369
+
370
+
338
371
  class StepConfig(BaseModel):
339
372
  """One pipeline step. Its type IS the key it carries: exactly one of
340
373
  `agent`, `action`, or `command` - empty values do not count, anything
@@ -354,6 +387,7 @@ class StepConfig(BaseModel):
354
387
  timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
355
388
  terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
356
389
  stop_when: StopWhenSpec | None = None # agent steps only (032)
390
+ brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
357
391
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
358
392
  setup: str | None = None # BR-13: command steps only
359
393
  network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
@@ -426,6 +460,20 @@ class StepConfig(BaseModel):
426
460
  raise ValueError(
427
461
  f"step {self.name!r}: 'stop_when' requires 'agent'"
428
462
  )
463
+ if self.brief_inputs is not None and self.agent is None:
464
+ raise ValueError(
465
+ f"step {self.name!r}: 'brief_inputs' requires 'agent'"
466
+ )
467
+ if (self.brief_inputs is not None
468
+ and (self.artifact is not None or self.verdicts is not None)):
469
+ raise ValueError(
470
+ f"step {self.name!r}: 'brief_inputs' is only valid on a "
471
+ "produces-only agent step"
472
+ )
473
+ if self.brief_inputs is not None and not self.produces:
474
+ raise ValueError(
475
+ f"step {self.name!r}: 'brief_inputs' requires 'produces'"
476
+ )
429
477
  if self.setup is not None and self.command is None and self.agent is None:
430
478
  raise ValueError(
431
479
  f"step {self.name!r}: 'setup' is only allowed on 'command' or 'agent' steps"
@@ -0,0 +1,6 @@
1
+ | Code | Meaning | Correct response |
2
+ | --- | --- | --- |
3
+ | AE-SBX-008 | The sandbox refused a git mutation command. Only the single commit step may change git state; agents never commit, branch, tag, or otherwise mutate git. | Do not run the command or any git mutation again. Continue without touching git and leave committing to the commit step. |
4
+ | AE-SBX-010 | The sandbox could not create the execution container for the command, so the command did not run. | The command is not the problem: retry once; if it persists, mention the code in your final response and stop. |
5
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
6
+ | AE-SBX-018 | The network egress proxy for this step stopped, so the sandbox refused the command rather than run without enforcement. | The command is not the problem: do not retry it, mention the code in your final response, and stop. |
@@ -0,0 +1,26 @@
1
+ """Bundled agent-facing error guide (034).
2
+
3
+ The guide table is an engine-owned resource, versioned with the engine so the
4
+ codes and their guidance cannot drift. Every agent step's task context gets
5
+ the table injected at assembly time; command and action steps never do (no
6
+ agent reads them).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from importlib.resources import files
12
+
13
+ GUIDE_RESOURCE = "agent_error_guide.md"
14
+ GUIDE_HEADING = "Agent-facing error guide"
15
+
16
+
17
+ def load_guide() -> str:
18
+ """The bundled guide table text."""
19
+ return (files("agentic_dev_loop.engine")
20
+ .joinpath(GUIDE_RESOURCE)
21
+ .read_text(encoding="utf-8"))
22
+
23
+
24
+ def guide_section() -> str:
25
+ """The guide block injected into an agent step's task context."""
26
+ return f"## {GUIDE_HEADING}\n\n{load_guide()}"
@@ -17,6 +17,7 @@ from .hostboundary import check_orchestrator_install, check_tooling_integrity
17
17
  from .loaders import resolve_docs
18
18
  from .isolation import (isolation_state_dir, resolve_effective_profile,
19
19
  resolve_image_ref, resolved_seccomp_profile)
20
+ from .stages import validate_brief_inputs
20
21
  from .registry import (Registry, default_registry, load_extensions,
21
22
  validate_action_mounts_egress, validate_action_network)
22
23
  from .secrets import scan_env_files
@@ -137,8 +138,10 @@ def dry_run(cfg_path, args, registry: Registry | None = None) -> int:
137
138
  _validate_agents(cfg, base / cfg.agents_dir)
138
139
  _validate_actions(cfg, registry)
139
140
  resolved_seccomp_profile(cfg) # fails loud on an unknown bundled:<name>
141
+ for step in cfg.steps:
142
+ validate_brief_inputs(Path.cwd(), step)
140
143
 
141
- loader, docs, subject = resolve_docs(cfg, base, registry, args)
144
+ loader, docs, subject, _doc_sources = resolve_docs(cfg, base, registry, args)
142
145
  image, memory, cpus = _resolved_image(cfg)
143
146
  state_dir = isolation_state_dir(cfg)
144
147
 
@@ -21,8 +21,15 @@ from pathlib import Path
21
21
  _DOC_SUFFIXES = (".md", ".yaml", ".yml", ".txt")
22
22
 
23
23
 
24
- def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
24
+ def resolve_unit_docs_with_sources(
25
+ docs_dir: Path | str, unit_slug: str
26
+ ) -> tuple[dict[str, str], dict[str, Path]]:
27
+ """Resolve one AI-DLC unit into (docs, sources). ``docs`` maps each
28
+ doc's workspace-relative key to its content; ``sources`` maps the same
29
+ key to its on-disk file so the brief regeneration gate can re-read it
30
+ (033 D4)."""
25
31
  docs: dict[str, str] = {}
32
+ sources: dict[str, Path] = {}
26
33
  docs_dir = Path(docs_dir)
27
34
  unit_slug = unit_slug.strip()
28
35
 
@@ -47,6 +54,7 @@ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
47
54
  if file.is_file() and file.suffix in _DOC_SUFFIXES:
48
55
  key = file.relative_to(unit_dir).as_posix()
49
56
  docs[key] = file.read_text(encoding="utf-8")
57
+ sources[key] = file
50
58
 
51
59
  # At this point docs holds only unit-dir files - an empty load means the
52
60
  # pipeline would build context from inception docs alone (the 2026-07-19
@@ -63,6 +71,7 @@ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
63
71
  for file in sorted(inception.iterdir()):
64
72
  if file.is_file() and file.suffix in (".md", ".txt"):
65
73
  docs[f"inception/{file.name}"] = file.read_text(encoding="utf-8")
74
+ sources[f"inception/{file.name}"] = file
66
75
 
67
76
  # The unit's code-generation plans are the build spec - absent is fine,
68
77
  # present is how the spec reaches agents.
@@ -70,7 +79,13 @@ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
70
79
  if plans_dir.is_dir():
71
80
  for file in sorted(plans_dir.glob(f"{unit_slug}-*-code-generation-plan.md")):
72
81
  docs[f"plans/{file.name}"] = file.read_text(encoding="utf-8")
82
+ sources[f"plans/{file.name}"] = file
83
+
84
+ return docs, sources
85
+
73
86
 
87
+ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
88
+ docs, _ = resolve_unit_docs_with_sources(docs_dir, unit_slug)
74
89
  return docs
75
90
 
76
91
 
@@ -116,6 +131,13 @@ class AidlcDocumentLoader:
116
131
  unit = args.aidlc_unit or get_current_unit(self.docs_dir)
117
132
  return resolve_unit_docs(self.docs_dir, unit), f"unit {unit}"
118
133
 
134
+ def sources(self, args: argparse.Namespace) -> dict[str, Path]:
135
+ """Source path for every resolved doc key (033): the regeneration
136
+ gate re-reads these from disk so a mid-run unit-plan edit is detected."""
137
+ unit = args.aidlc_unit or get_current_unit(self.docs_dir)
138
+ _, sources = resolve_unit_docs_with_sources(self.docs_dir, unit)
139
+ return sources
140
+
119
141
 
120
142
  class AidlcExtension:
121
143
  name = "ai-dlc"
@@ -28,8 +28,9 @@ from .secrets import resolve_secrets, scan_env_files
28
28
  from .registry import (Registry, default_registry, load_extensions,
29
29
  validate_action_mounts_egress, validate_action_network)
30
30
  from .stages import (Runtime, clear_attempt_artifacts,
31
- make_action_node, make_agent_node, make_bump_node,
32
- make_command_node, make_router, success_node)
31
+ make_action_node, make_agent_node, make_brief_node,
32
+ make_bump_node, make_command_node, make_router,
33
+ success_node)
33
34
 
34
35
  CHECKPOINT_DB = "loop-checkpoints.sqlite" # under state_dir, one DB per workspace
35
36
 
@@ -47,6 +48,7 @@ class LoopState(TypedDict):
47
48
  every other key is last-write-wins."""
48
49
  subject: str
49
50
  docs: dict[str, str]
51
+ doc_sources: dict[str, str] # doc key -> source path (033 regeneration gate)
50
52
  artifacts: Annotated[dict[str, str], _merge_artifacts] # step name -> artifact text
51
53
  attempt: int
52
54
  max_attempts: int
@@ -61,6 +63,7 @@ class LoopState(TypedDict):
61
63
  step_verdicts: dict[str, str | None] # per-step pass/fail/None(unclear)
62
64
  retry_target: str | None # where the bump node loops back to
63
65
  thread_id: str
66
+ brief_hashes: Annotated[dict[str, str], _merge_artifacts] # step name -> input hash (033)
64
67
 
65
68
 
66
69
  def build_graph(runtime: Runtime, checkpointer=None):
@@ -83,7 +86,10 @@ def build_graph(runtime: Runtime, checkpointer=None):
83
86
  graph = StateGraph(LoopState)
84
87
  for step in runtime.cfg.steps:
85
88
  if step.kind == "agent":
86
- node = make_agent_node(runtime, step)
89
+ if step.brief_inputs is not None:
90
+ node = make_brief_node(runtime, step)
91
+ else:
92
+ node = make_agent_node(runtime, step)
87
93
  elif step.kind == "command":
88
94
  node = make_command_node(runtime, step)
89
95
  else:
@@ -178,14 +184,15 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
178
184
  if resume:
179
185
  initial = None # state (docs/subject/artifacts) comes from the checkpoint
180
186
  else:
181
- _loader, docs, subject = resolve_docs(cfg, base, registry, args)
187
+ _loader, docs, subject, doc_sources = resolve_docs(cfg, base, registry, args)
182
188
  # A fresh run owns state_dir: drop cross-run leftovers (a previous
183
189
  # failure's last-error.md, stale artifacts) so nothing mistakes
184
190
  # them for this run's output. --resume keeps them.
185
- clear_attempt_artifacts(runtime)
191
+ clear_attempt_artifacts(runtime, exempt_run_scoped=False)
186
192
  initial: LoopState = {
187
193
  "subject": subject,
188
194
  "docs": docs,
195
+ "doc_sources": doc_sources,
189
196
  "artifacts": {},
190
197
  "attempt": 1,
191
198
  "max_attempts": max_attempts,
@@ -204,6 +211,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
204
211
  "step_verdicts": {},
205
212
  "retry_target": None,
206
213
  "thread_id": thread_id,
214
+ "brief_hashes": {},
207
215
  }
208
216
  bus = viz_bus
209
217
  with SqliteSaver.from_conn_string(db) as saver:
@@ -71,6 +71,12 @@ class PlanDocumentLoader:
71
71
  docs = resolve_plan_docs(args.plan)
72
72
  return docs, f"plan {args.plan.name}"
73
73
 
74
+ def sources(self, args: argparse.Namespace) -> dict[str, Path]:
75
+ """Source path for every resolved doc key (033): the regeneration
76
+ gate re-reads these from disk so a mid-run plan edit is detected."""
77
+ path = Path(args.plan)
78
+ return {path.name: path}
79
+
74
80
 
75
81
  def select_loader(registry: Registry, args: argparse.Namespace) -> DocumentLoader | None:
76
82
  """The document-loader coexistence model - load many, invoke one:
@@ -103,11 +109,15 @@ def select_loader(registry: Registry, args: argparse.Namespace) -> DocumentLoade
103
109
 
104
110
 
105
111
  def resolve_docs(cfg: PipelineConfig, base: Path, registry: Registry,
106
- args) -> tuple[DocumentLoader | None, dict[str, str], str]:
112
+ args) -> tuple[DocumentLoader | None, dict[str, str], str,
113
+ dict[str, str]]:
107
114
  """Select and invoke the document loader, then append the pipeline's
108
115
  additional_files (resolved relative to the pipeline file's dir;
109
- listed-but-missing fails fast). Returns (loader, docs, subject)."""
116
+ listed-but-missing fails fast). Returns (loader, docs, subject,
117
+ doc_sources) - the source path for every loader/additional_files doc key
118
+ so the brief regeneration gate can re-read them from disk (033 D4)."""
110
119
  loader = select_loader(registry, args)
120
+ sources: dict[str, str] = {}
111
121
  if loader is not None:
112
122
  docs, subject = loader.resolve(args)
113
123
  if not docs:
@@ -116,6 +126,10 @@ def resolve_docs(cfg: PipelineConfig, base: Path, registry: Registry,
116
126
  raise RuntimeError(
117
127
  f"Document loader {loader.name!r} returned no docs - "
118
128
  "refusing to build context from nothing")
129
+ loader_sources = getattr(loader, "sources", None)
130
+ if callable(loader_sources):
131
+ for name, path in loader_sources(args).items():
132
+ sources[name] = str(Path(path).resolve())
119
133
  else:
120
134
  docs, subject = {}, cfg.name
121
135
  for rel in cfg.additional_files:
@@ -123,4 +137,5 @@ def resolve_docs(cfg: PipelineConfig, base: Path, registry: Registry,
123
137
  if not path.is_file():
124
138
  raise FileNotFoundError(f"additional_files entry not found: {path}")
125
139
  docs[path.name] = path.read_text(encoding="utf-8")
126
- return loader, docs, subject
140
+ sources[path.name] = str(path.resolve())
141
+ return loader, docs, subject, sources