agentic-dev-loop 0.10.2__tar.gz → 0.10.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {agentic_dev_loop-0.10.2/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.10.4}/PKG-INFO +19 -1
  2. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/README.md +18 -0
  3. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/pyproject.toml +1 -1
  4. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/extensions/aidlc.py +10 -4
  5. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/forbidden.py +12 -26
  6. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +37 -4
  7. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +25 -4
  8. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/stages.py +1 -2
  9. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4/src/agentic_dev_loop.egg-info}/PKG-INFO +19 -1
  10. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_036_banned_commands_and_lane_boundary.py +7 -43
  11. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_recipes.py +6 -0
  12. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_aidlc.py +15 -0
  13. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_publish.py +11 -58
  14. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/LICENSE +0 -0
  15. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/setup.cfg +0 -0
  16. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/__init__.py +0 -0
  17. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/__init__.py +0 -0
  18. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/config.py +0 -0
  19. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/paths.py +0 -0
  20. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/profiles/__init__.py +0 -0
  21. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +0 -0
  22. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/profiles/data/python.yaml +0 -0
  23. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/results.py +0 -0
  24. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/core/verdict.py +0 -0
  25. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/__init__.py +0 -0
  26. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/actions.py +0 -0
  27. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  28. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  29. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/agents.py +0 -0
  30. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  31. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  32. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  33. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  34. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/cli.py +0 -0
  35. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/containers.py +0 -0
  36. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/deps.py +0 -0
  37. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  38. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  39. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  40. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  41. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/graph.py +0 -0
  42. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  43. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/isolation.py +0 -0
  44. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/llm.py +0 -0
  45. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/loaders.py +0 -0
  46. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/mounts.py +0 -0
  47. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/__init__.py +0 -0
  48. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/checks.py +0 -0
  49. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/doctor.py +0 -0
  50. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/init.py +0 -0
  51. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/recipes.py +0 -0
  52. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +0 -0
  53. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/templates/env.example +0 -0
  54. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +0 -0
  55. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +0 -0
  56. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +0 -0
  57. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/onboarding/templates.py +0 -0
  58. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/outputs.py +0 -0
  59. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/presets.py +0 -0
  60. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/proc.py +0 -0
  61. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/registry.py +0 -0
  62. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  63. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  64. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/secrets.py +0 -0
  65. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  66. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  67. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/py.typed +0 -0
  68. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/viz/__init__.py +0 -0
  69. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/viz/__main__.py +0 -0
  70. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop/viz/server.py +0 -0
  71. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  72. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  73. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  74. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  75. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  76. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_029_merge_on_conflict.py +0 -0
  77. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_033_brief_step.py +0 -0
  78. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_035_finalize_nudge.py +0 -0
  79. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_041_brief_coverage.py +0 -0
  80. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_046_brief_prompt.py +0 -0
  81. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_cli.py +0 -0
  82. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_doctor.py +0 -0
  83. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_init.py +0 -0
  84. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_profile_loading.py +0 -0
  85. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_profiles.py +0 -0
  86. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_schema_lock.py +0 -0
  87. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_055_templates.py +0 -0
  88. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_actions.py +0 -0
  89. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_agent_error_guide.py +0 -0
  90. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_agents.py +0 -0
  91. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_cli.py +0 -0
  92. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_config.py +0 -0
  93. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_containers.py +0 -0
  94. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_deps.py +0 -0
  95. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_dry_run.py +0 -0
  96. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_e2e_fixtures.py +0 -0
  97. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_egress_proxy.py +0 -0
  98. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_graph.py +0 -0
  99. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_hostboundary.py +0 -0
  100. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_hostboundary_integration.py +0 -0
  101. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_isolation.py +0 -0
  102. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_isolation_integration.py +0 -0
  103. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_isolation_pbt.py +0 -0
  104. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_llm.py +0 -0
  105. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_loaders.py +0 -0
  106. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_module_contract.py +0 -0
  107. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_outputs.py +0 -0
  108. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_outputs_pbt.py +0 -0
  109. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_proc.py +0 -0
  110. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_qa_prompt.py +0 -0
  111. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_registry.py +0 -0
  112. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_repo_ascii.py +0 -0
  113. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_results.py +0 -0
  114. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_sdk_compat.py +0 -0
  115. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_secrets.py +0 -0
  116. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_secrets_integration.py +0 -0
  117. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_secrets_pipeline.py +0 -0
  118. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_source_resolution.py +0 -0
  119. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_stages.py +0 -0
  120. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_step_workspace.py +0 -0
  121. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_sync_back.py +0 -0
  122. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_trigger_script.py +0 -0
  123. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_unit5_config.py +0 -0
  124. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_unit5_pbt.py +0 -0
  125. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_unit5_pure.py +0 -0
  126. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_verdict.py +0 -0
  127. {agentic_dev_loop-0.10.2 → agentic_dev_loop-0.10.4}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.10.2
3
+ Version: 0.10.4
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
@@ -229,6 +229,7 @@ steps:
229
229
  agent: dev
230
230
  produces: implementation-summary.md
231
231
  terminal_timeout: 900
232
+ # forbidden_commands: ["npm install", "npm ci"] # per-step tool deny-list; engine defaults are git mutations + docker
232
233
  on_pass: test
233
234
 
234
235
  - name: test
@@ -268,6 +269,7 @@ steps:
268
269
  | `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. |
269
270
  | `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. |
270
271
  | `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). |
272
+ | `forbidden_commands:` | Optional per-step list of regex patterns. The sandbox terminal matches them against each agent command before any shell spawn; a match returns a structured `[forbidden: <label>]` observation instead of running. A single string is coerced to a one-item list; an invalid regex fails at load. Defaults are run invariants only (git mutations, docker); tool-specific rules belong to the toolchain profile/recipe or the pipeline author. Command steps are unaffected. See [Banned commands](#banned-commands-forbidden_commands). |
271
273
  | `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. |
272
274
  | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
273
275
  | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
@@ -284,6 +286,22 @@ steps:
284
286
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored 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). |
285
287
  | `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). |
286
288
 
289
+ ### Banned commands (`forbidden_commands:`)
290
+
291
+ `forbidden_commands:` is a per-step list of regex patterns. The sandbox
292
+ terminal checks them against every command the agent runs, before any shell
293
+ spawn: a match returns a structured `[forbidden: <label>]` observation instead
294
+ of executing. The engine defaults are run invariants only - git mutations and
295
+ docker - so the deny-list is network-independent by design: `network:` stays a
296
+ docker/egress concern. A single string is coerced to a one-item list; an
297
+ invalid regex is a load error. Command steps bypass the filter by design.
298
+
299
+ Tool-specific rules are not engine defaults. A toolchain profile or bundled
300
+ recipe declares them per step (for example the `python` recipe denies
301
+ `pip install`, `uv add`, and `pip-compile` on its agent steps), and a
302
+ consumer pipeline authors its own for its toolchain. There is no disable
303
+ escape hatch for the engine defaults.
304
+
287
305
  ### Reasoning effort (opt-in)
288
306
 
289
307
  `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
@@ -185,6 +185,7 @@ steps:
185
185
  agent: dev
186
186
  produces: implementation-summary.md
187
187
  terminal_timeout: 900
188
+ # forbidden_commands: ["npm install", "npm ci"] # per-step tool deny-list; engine defaults are git mutations + docker
188
189
  on_pass: test
189
190
 
190
191
  - name: test
@@ -224,6 +225,7 @@ steps:
224
225
  | `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. |
225
226
  | `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. |
226
227
  | `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). |
228
+ | `forbidden_commands:` | Optional per-step list of regex patterns. The sandbox terminal matches them against each agent command before any shell spawn; a match returns a structured `[forbidden: <label>]` observation instead of running. A single string is coerced to a one-item list; an invalid regex fails at load. Defaults are run invariants only (git mutations, docker); tool-specific rules belong to the toolchain profile/recipe or the pipeline author. Command steps are unaffected. See [Banned commands](#banned-commands-forbidden_commands). |
227
229
  | `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. |
228
230
  | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
229
231
  | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
@@ -240,6 +242,22 @@ steps:
240
242
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored 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). |
241
243
  | `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). |
242
244
 
245
+ ### Banned commands (`forbidden_commands:`)
246
+
247
+ `forbidden_commands:` is a per-step list of regex patterns. The sandbox
248
+ terminal checks them against every command the agent runs, before any shell
249
+ spawn: a match returns a structured `[forbidden: <label>]` observation instead
250
+ of executing. The engine defaults are run invariants only - git mutations and
251
+ docker - so the deny-list is network-independent by design: `network:` stays a
252
+ docker/egress concern. A single string is coerced to a one-item list; an
253
+ invalid regex is a load error. Command steps bypass the filter by design.
254
+
255
+ Tool-specific rules are not engine defaults. A toolchain profile or bundled
256
+ recipe declares them per step (for example the `python` recipe denies
257
+ `pip install`, `uv add`, and `pip-compile` on its agent steps), and a
258
+ consumer pipeline authors its own for its toolchain. There is no disable
259
+ escape hatch for the engine defaults.
260
+
243
261
  ### Reasoning effort (opt-in)
244
262
 
245
263
  `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "0.10.2"
7
+ version = "0.10.4"
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"
@@ -74,12 +74,18 @@ def resolve_unit_docs_with_sources(
74
74
  sources[f"inception/{file.name}"] = file
75
75
 
76
76
  # The unit's code-generation plans are the build spec - absent is fine,
77
- # present is how the spec reaches agents.
77
+ # present is how the spec reaches agents. Two shapes are in the wild:
78
+ # bare (`unit-3-code-generation-plan.md`) and infixed
79
+ # (`unit-3-scan-core-types-code-generation-plan.md`); the bare shape must
80
+ # not be lost, and neither pattern may leak into a slug that merely
81
+ # extends this one (unit-3 must not match a unit-30 plan).
78
82
  plans_dir = docs_dir / "construction" / "plans"
79
83
  if plans_dir.is_dir():
80
- for file in sorted(plans_dir.glob(f"{unit_slug}-*-code-generation-plan.md")):
81
- docs[f"plans/{file.name}"] = file.read_text(encoding="utf-8")
82
- sources[f"plans/{file.name}"] = file
84
+ for pattern in (f"{unit_slug}-code-generation-plan.md",
85
+ f"{unit_slug}-*-code-generation-plan.md"):
86
+ for file in sorted(plans_dir.glob(pattern)):
87
+ docs[f"plans/{file.name}"] = file.read_text(encoding="utf-8")
88
+ sources[f"plans/{file.name}"] = file
83
89
 
84
90
  return docs, sources
85
91
 
@@ -1,4 +1,4 @@
1
- """Banned-command deny-list (036 D1).
1
+ """Banned-command deny-list (036 D1, revised by 056).
2
2
 
3
3
  The sandbox terminal enforces a deny-list before any shell spawn: a command
4
4
  whose argv matches a step's ``forbidden_commands:`` regex (merged with the
@@ -6,11 +6,15 @@ engine defaults) returns a structured observation instead of reaching the
6
6
  sandbox. Command steps are unaffected - they are declared infrastructure and
7
7
  already fail fast on their exit code.
8
8
 
9
+ The defaults are run invariants only: git mutations and docker.
10
+ Tool-specific rules are not engine defaults - a step declares them with
11
+ ``forbidden_commands`` (the bundled onboarding recipes carry the engine's
12
+ own toolchain guidance). The deny-list is network-independent by design;
13
+ network is a sandbox egress concern.
14
+
9
15
  The default list carries human-readable labels; custom entries use their
10
- regex text as the label. ``dotnet restore`` and ``dotnet add`` are network
11
- gated: denied only while the step's ``network:`` flag is off.
16
+ regex text as the label.
12
17
  """
13
-
14
18
  from __future__ import annotations
15
19
 
16
20
  import re
@@ -30,22 +34,9 @@ _DEFAULT_PATTERNS: tuple[tuple[str, str], ...] = (
30
34
  ("git checkout", r"\bgit\s+checkout\b"),
31
35
  ("git reset", r"\bgit\s+reset\b"),
32
36
  ("git tag", r"\bgit\s+tag\b"),
33
- ("npm install", r"\bnpm\s+install\b"),
34
- ("npm ci", r"\bnpm\s+ci\b"),
35
- ("pip install", r"\bpip\s+install\b"),
36
- ("uv add", r"\buv\s+add\b"),
37
- ("pip-compile", r"\bpip-compile\b"),
38
- ("dotnet add package", r"\bdotnet\s+add\s+package\b"),
39
- ("npx playwright", r"\bnpx\s+playwright\b"),
40
- ("playwright", r"\bplaywright\b"),
41
37
  ("docker", r"\bdocker\b"),
42
38
  )
43
39
 
44
- _NETWORK_GATED_PATTERNS: tuple[tuple[str, str], ...] = (
45
- ("dotnet restore", r"\bdotnet\s+restore\b"),
46
- ("dotnet add", r"\bdotnet\s+add\b"),
47
- )
48
-
49
40
 
50
41
  def _compile(label: str, pattern: str) -> ForbiddenRule:
51
42
  return ForbiddenRule(label=label, regex=re.compile(pattern, re.IGNORECASE))
@@ -54,21 +45,16 @@ def _compile(label: str, pattern: str) -> ForbiddenRule:
54
45
  DEFAULT_FORBIDDEN_RULES: tuple[ForbiddenRule, ...] = tuple(
55
46
  _compile(label, pattern) for label, pattern in _DEFAULT_PATTERNS)
56
47
 
57
- NETWORK_GATED_FORBIDDEN_RULES: tuple[ForbiddenRule, ...] = tuple(
58
- _compile(label, pattern) for label, pattern in _NETWORK_GATED_PATTERNS)
59
-
60
48
 
61
- def resolve_forbidden_rules(custom_patterns: list[str] | tuple[str, ...] | None,
62
- network: bool) -> tuple[ForbiddenRule, ...]:
49
+ def resolve_forbidden_rules(
50
+ custom_patterns: list[str] | tuple[str, ...] | None,
51
+ ) -> tuple[ForbiddenRule, ...]:
63
52
  """Merge the engine defaults with a step's custom regex list.
64
53
 
65
54
  Custom entries are appended after the defaults, so the first match wins
66
- on overlap. Network-gated defaults are included only while ``network`` is
67
- off; there is no disable escape hatch for the engine defaults.
55
+ on overlap; there is no disable escape hatch for the engine defaults.
68
56
  """
69
57
  rules = list(DEFAULT_FORBIDDEN_RULES)
70
- if not network:
71
- rules.extend(NETWORK_GATED_FORBIDDEN_RULES)
72
58
  for pattern in custom_patterns or ():
73
59
  rules.append(_compile(pattern, pattern))
74
60
  return tuple(rules)
@@ -14,7 +14,18 @@ dev_impossible_set:
14
14
  - "database-category tests"
15
15
  - "commands that boot servers or need external services"
16
16
  steps:
17
- - {name: brief, kind: brief, brief_inputs: {include: [AGENTS.md]}, on_pass: dev}
17
+ - name: brief
18
+ kind: brief
19
+ brief_inputs: {include: [AGENTS.md]}
20
+ forbidden_commands:
21
+ - npm install
22
+ - npm ci
23
+ - dotnet add package
24
+ - dotnet restore
25
+ - dotnet add
26
+ - npx playwright
27
+ - playwright
28
+ on_pass: dev
18
29
  - name: dev
19
30
  kind: agent
20
31
  agent: dev
@@ -22,6 +33,14 @@ steps:
22
33
  max_iterations: 100
23
34
  finalize_nudge_turns: 10
24
35
  produces: implementation-summary.md
36
+ forbidden_commands:
37
+ - npm install
38
+ - npm ci
39
+ - dotnet add package
40
+ - dotnet restore
41
+ - dotnet add
42
+ - npx playwright
43
+ - playwright
25
44
  setup: 'cd __FRONTEND_DIR__ && npm ci && cd .. && dotnet restore __SOLUTION__'
26
45
  mounts: [{path: ".", mode: rw}]
27
46
  on_pass: restore
@@ -62,9 +81,23 @@ steps:
62
81
  optional: {detect: tools/run-conformance.sh}
63
82
  on_pass: review
64
83
  on_fail: {goto: dev, retry: true}
65
- - {name: review, kind: agent, agent: review, reasoning_effort: xhigh,
66
- artifact: review-findings.md, verdicts: {pass: APPROVED, fail: NEEDS CHANGES},
67
- mounts: [{path: ".", mode: ro}], on_pass: qa, on_fail: {goto: dev, retry: true}}
84
+ - name: review
85
+ kind: agent
86
+ agent: review
87
+ reasoning_effort: xhigh
88
+ artifact: review-findings.md
89
+ verdicts: {pass: APPROVED, fail: NEEDS CHANGES}
90
+ mounts: [{path: ".", mode: ro}]
91
+ forbidden_commands:
92
+ - npm install
93
+ - npm ci
94
+ - dotnet add package
95
+ - dotnet restore
96
+ - dotnet add
97
+ - npx playwright
98
+ - playwright
99
+ on_pass: qa
100
+ on_fail: {goto: dev, retry: true}
68
101
  - name: qa
69
102
  kind: agent
70
103
  agent: qa
@@ -11,7 +11,14 @@ brief_env_facts:
11
11
  dev_impossible_set:
12
12
  - "package-manager fetches after a new pin run only in the network-on restore step"
13
13
  steps:
14
- - {name: brief, kind: brief, brief_inputs: {include: [AGENTS.md]}, on_pass: dev}
14
+ - name: brief
15
+ kind: brief
16
+ brief_inputs: {include: [AGENTS.md]}
17
+ forbidden_commands:
18
+ - pip install
19
+ - uv add
20
+ - pip-compile
21
+ on_pass: dev
15
22
  - name: dev
16
23
  kind: agent
17
24
  agent: dev
@@ -19,6 +26,10 @@ steps:
19
26
  max_iterations: 150
20
27
  finalize_nudge_turns: 10
21
28
  produces: implementation-summary.md
29
+ forbidden_commands:
30
+ - pip install
31
+ - uv add
32
+ - pip-compile
22
33
  setup: 'python -m venv __VENV__ && __VENV__/bin/pip install -e "__DEV_EXTRAS__"'
23
34
  mounts: [{path: ".", mode: rw}]
24
35
  on_pass: restore
@@ -38,9 +49,19 @@ steps:
38
49
  mounts: [{path: ".", mode: rw}]
39
50
  on_pass: review
40
51
  on_fail: {goto: dev, retry: true}
41
- - {name: review, kind: agent, agent: review, reasoning_effort: xhigh,
42
- artifact: review-findings.md, verdicts: {pass: APPROVED, fail: NEEDS CHANGES},
43
- mounts: [{path: ".", mode: ro}], on_pass: qa, on_fail: {goto: dev, retry: true}}
52
+ - name: review
53
+ kind: agent
54
+ agent: review
55
+ reasoning_effort: xhigh
56
+ artifact: review-findings.md
57
+ verdicts: {pass: APPROVED, fail: NEEDS CHANGES}
58
+ mounts: [{path: ".", mode: ro}]
59
+ forbidden_commands:
60
+ - pip install
61
+ - uv add
62
+ - pip-compile
63
+ on_pass: qa
64
+ on_fail: {goto: dev, retry: true}
44
65
  - name: qa
45
66
  kind: agent
46
67
  agent: qa
@@ -158,8 +158,7 @@ class Runtime:
158
158
  else:
159
159
  cwd, mount_set, egress_policy = (self.workspace, None, None)
160
160
  forbidden_commands = (
161
- resolve_forbidden_rules(step.forbidden_commands,
162
- step.network)
161
+ resolve_forbidden_rules(step.forbidden_commands)
163
162
  if step is not None else None)
164
163
  allowlist = (
165
164
  tuple(self.engine_allowlist)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.10.2
3
+ Version: 0.10.4
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
@@ -229,6 +229,7 @@ steps:
229
229
  agent: dev
230
230
  produces: implementation-summary.md
231
231
  terminal_timeout: 900
232
+ # forbidden_commands: ["npm install", "npm ci"] # per-step tool deny-list; engine defaults are git mutations + docker
232
233
  on_pass: test
233
234
 
234
235
  - name: test
@@ -268,6 +269,7 @@ steps:
268
269
  | `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. |
269
270
  | `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. |
270
271
  | `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). |
272
+ | `forbidden_commands:` | Optional per-step list of regex patterns. The sandbox terminal matches them against each agent command before any shell spawn; a match returns a structured `[forbidden: <label>]` observation instead of running. A single string is coerced to a one-item list; an invalid regex fails at load. Defaults are run invariants only (git mutations, docker); tool-specific rules belong to the toolchain profile/recipe or the pipeline author. Command steps are unaffected. See [Banned commands](#banned-commands-forbidden_commands). |
271
273
  | `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. |
272
274
  | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
273
275
  | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
@@ -284,6 +286,22 @@ steps:
284
286
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored 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). |
285
287
  | `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). |
286
288
 
289
+ ### Banned commands (`forbidden_commands:`)
290
+
291
+ `forbidden_commands:` is a per-step list of regex patterns. The sandbox
292
+ terminal checks them against every command the agent runs, before any shell
293
+ spawn: a match returns a structured `[forbidden: <label>]` observation instead
294
+ of executing. The engine defaults are run invariants only - git mutations and
295
+ docker - so the deny-list is network-independent by design: `network:` stays a
296
+ docker/egress concern. A single string is coerced to a one-item list; an
297
+ invalid regex is a load error. Command steps bypass the filter by design.
298
+
299
+ Tool-specific rules are not engine defaults. A toolchain profile or bundled
300
+ recipe declares them per step (for example the `python` recipe denies
301
+ `pip install`, `uv add`, and `pip-compile` on its agent steps), and a
302
+ consumer pipeline authors its own for its toolchain. There is no disable
303
+ escape hatch for the engine defaults.
304
+
287
305
  ### Reasoning effort (opt-in)
288
306
 
289
307
  `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
@@ -48,34 +48,21 @@ def _export(tmp_path: Path, ws: Path) -> Path:
48
48
 
49
49
  class TestForbiddenCommandRules:
50
50
  def test_defaults_cover_deny_list(self):
51
- rules = resolve_forbidden_rules([], network=False)
51
+ rules = resolve_forbidden_rules([])
52
52
  labels = [r.label for r in rules]
53
- for expected in ("git commit", "git push", "git rebase", "git checkout",
54
- "git reset", "git tag", "npm install", "npm ci",
55
- "pip install", "uv add", "pip-compile",
56
- "dotnet add package", "npx playwright", "playwright",
57
- "docker", "dotnet restore", "dotnet add"):
58
- assert expected in labels
59
-
60
- def test_network_on_drops_network_gated_dotnet(self):
61
- offline = resolve_forbidden_rules([], network=False)
62
- online = resolve_forbidden_rules([], network=True)
63
- assert "dotnet restore" in [r.label for r in offline]
64
- assert "dotnet add" in [r.label for r in offline]
65
- assert "dotnet restore" not in [r.label for r in online]
66
- assert "dotnet add" not in [r.label for r in online]
53
+ assert labels == ['git commit', 'git push', 'git rebase', 'git checkout', 'git reset', 'git tag', 'docker']
67
54
 
68
55
  def test_custom_rules_append_with_pattern_label(self):
69
- rules = resolve_forbidden_rules([r"rm\s+-rf"], network=False)
56
+ rules = resolve_forbidden_rules([r"rm\s+-rf"])
70
57
  assert rules[-1].label == r"rm\s+-rf"
71
58
 
72
59
  def test_shell_wrapped_argv_matches(self):
73
60
  argv = ["/bin/sh", "-c", "git push origin main"]
74
61
  assert match_forbidden_command(
75
- argv, resolve_forbidden_rules([], network=False)) == "git push"
62
+ argv, resolve_forbidden_rules([])) == "git push"
76
63
 
77
64
  def test_empty_argv_never_matches(self):
78
- assert match_forbidden_command([], resolve_forbidden_rules([], False)) is None
65
+ assert match_forbidden_command([], resolve_forbidden_rules([])) is None
79
66
 
80
67
 
81
68
  class TestSandboxTerminalDenyList:
@@ -86,7 +73,7 @@ class TestSandboxTerminalDenyList:
86
73
  from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
87
74
  ex = SandboxTerminalExecutor(
88
75
  fake_sandbox, workspace=str(tmp_path), env={},
89
- forbidden_commands=resolve_forbidden_rules([], network=False))
76
+ forbidden_commands=resolve_forbidden_rules([]))
90
77
  obs = ex(TerminalAction(command="git commit -m x"), None)
91
78
  assert "[forbidden: git commit]" in obs.text
92
79
  assert obs.exit_code is None
@@ -98,34 +85,11 @@ class TestSandboxTerminalDenyList:
98
85
  from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
99
86
  ex = SandboxTerminalExecutor(
100
87
  fake_sandbox, workspace=str(tmp_path), env={},
101
- forbidden_commands=resolve_forbidden_rules([r"rm\s+-rf"], False))
88
+ forbidden_commands=resolve_forbidden_rules([r"rm\s+-rf"]))
102
89
  obs = ex(TerminalAction(command="rm -rf /tmp/x"), None)
103
90
  assert "[forbidden: rm\\s+-rf]" in obs.text
104
91
  assert fake_sandbox.requests == []
105
92
 
106
- def test_dotnet_restore_denied_offline(self, fake_sandbox, tmp_path):
107
- from openhands.tools.terminal import TerminalAction
108
-
109
- from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
110
- ex = SandboxTerminalExecutor(
111
- fake_sandbox, workspace=str(tmp_path), env={},
112
- forbidden_commands=resolve_forbidden_rules([], network=False))
113
- obs = ex(TerminalAction(command="dotnet restore"), None)
114
- assert "[forbidden: dotnet restore]" in obs.text
115
- assert fake_sandbox.requests == []
116
-
117
- def test_dotnet_restore_allowed_online(self, fake_sandbox, tmp_path):
118
- from openhands.tools.terminal import TerminalAction
119
-
120
- from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
121
- ex = SandboxTerminalExecutor(
122
- fake_sandbox, workspace=str(tmp_path), env={}, network=True,
123
- forbidden_commands=resolve_forbidden_rules([], network=True))
124
- fake_sandbox.script(lambda req: (0, "restored", False))
125
- obs = ex(TerminalAction(command="dotnet restore"), None)
126
- assert obs.exit_code == 0
127
- assert fake_sandbox.requests[0].argv == ["dotnet", "restore"]
128
-
129
93
 
130
94
  class TestPathAllowed:
131
95
  def test_literal_prefix(self):
@@ -47,6 +47,9 @@ def test_python_recipe_gate_set_and_commands():
47
47
  assert steps["qa"]["finalize_nudge_turns"] == 10
48
48
  assert "env" in steps["qa"]["verdicts"]
49
49
  assert "on_env" not in steps["qa"]
50
+ for name in ("brief", "dev", "review"):
51
+ assert steps[name]["forbidden_commands"] == ['pip install', 'uv add', 'pip-compile']
52
+ assert "forbidden_commands" not in steps["qa"]
50
53
 
51
54
 
52
55
  def test_dotnet_node_recipe_gate_set_and_commands():
@@ -69,6 +72,9 @@ def test_dotnet_node_recipe_gate_set_and_commands():
69
72
  assert steps["qa"]["env"] == {"PLAYWRIGHT_BROWSERS_PATH": "qa/.browsers"}
70
73
  assert steps["qa"]["mounts"] == [{"path": ".", "mode": "rw"}]
71
74
  assert len(doc["dev_impossible_set"]) == 4
75
+ for name in ("brief", "dev", "review"):
76
+ assert steps[name]["forbidden_commands"] == ['npm install', 'npm ci', 'dotnet add package', 'dotnet restore', 'dotnet add', 'npx playwright', 'playwright']
77
+ assert "forbidden_commands" not in steps["qa"]
72
78
 
73
79
 
74
80
  def test_unknown_recipe_raises():
@@ -135,6 +135,21 @@ class TestResolveUnitDocs:
135
135
  assert not any("nfr-design" in k for k in docs)
136
136
  assert not any("unit-2" in k for k in docs)
137
137
 
138
+ def test_loads_bare_slug_code_generation_plan(self, tmp_path):
139
+ """`{slug}-code-generation-plan.md` (no infix) is the build spec -
140
+ the repo's actual plan filenames, e.g. unit-3-code-generation-plan.md,
141
+ must load, not only infixed variants like unit-3-scan-*."""
142
+ unit_dir = tmp_path / "construction" / "unit-3"
143
+ unit_dir.mkdir(parents=True)
144
+ (unit_dir / "design.md").write_text("# Design")
145
+ plans = tmp_path / "construction" / "plans"
146
+ plans.mkdir(parents=True)
147
+ (plans / "unit-3-code-generation-plan.md").write_text("# Build spec")
148
+
149
+ docs = resolve_unit_docs(tmp_path, "unit-3")
150
+
151
+ assert docs["plans/unit-3-code-generation-plan.md"] == "# Build spec"
152
+
138
153
  def test_missing_plans_dir_is_not_an_error(self, tmp_path):
139
154
  unit_dir = tmp_path / "construction" / "unit-2"
140
155
  unit_dir.mkdir(parents=True)
@@ -264,79 +264,32 @@ class TestConsumerAdoption:
264
264
  assert proc.returncode == 0, proc.stderr
265
265
  assert read_version(repo) == "0.2.1"
266
266
 
267
- def test_049_marker_without_adoption_section_fails(self, tmp_path):
268
- """Plan 049 adds consumer pipeline obligations, so its slug must be
269
- registered: a release entry referencing it without a
270
- `### Consumer adoption` note gets refused."""
267
+ def test_dry_run_refuses_violating_entry(self, tmp_path):
268
+ """The dry run is the pre-cut gate: it runs the same refusal checks
269
+ as the real cut, so a violating entry is caught before anything is
270
+ written, committed, or tagged."""
271
271
  repo, _ = make_repo(
272
272
  tmp_path,
273
273
  unreleased=(
274
274
  "### Changed\n\n"
275
- "- feat: implement plan 049-native-brief-step-type\n"
276
- ),
277
- )
278
- proc = run_publish(repo, "--patch")
279
- assert proc.returncode != 0
280
- assert "Consumer adoption" in proc.stderr
281
- assert read_version(repo) == "0.2.0"
282
- assert tags(repo) == []
283
-
284
- def test_046_marker_without_adoption_section_fails(self, tmp_path):
285
- """Plan 046 obligates consumers to delete the vendored brief prompt
286
- and declare brief_inputs.sections, so its slug is registered too:
287
- a release entry referencing it without a `### Consumer adoption`
288
- note gets refused."""
289
- repo, _ = make_repo(
290
- tmp_path,
291
- unreleased=(
292
- "### Changed\n\n"
293
- "- feat: tweak plan 046-embed-brief-prompt\n"
294
- ),
295
- )
296
- proc = run_publish(repo, "--patch")
297
- assert proc.returncode != 0
298
- assert "Consumer adoption" in proc.stderr
299
- assert read_version(repo) == "0.2.0"
300
- assert tags(repo) == []
301
-
302
- def test_054_marker_without_adoption_section_fails(self, tmp_path):
303
- """Plan 054 flips reasoning passback to config-gated: pipelines
304
- whose llm:/llms: entries point at a thinking model must set
305
- reasoning_effort (absent effort now forwards nothing and raises a
306
- D12 engine error), so its slug is registered: a release entry
307
- referencing it without a `### Consumer adoption` note gets
308
- refused."""
309
- repo, _ = make_repo(
310
- tmp_path,
311
- unreleased=(
312
- "### Changed\n\n"
313
- "- feat: implement plan 054-per-step-model\n"
275
+ "- feat: construct plan 031-qa-environmental-verdict.md\n"
314
276
  ),
315
277
  )
316
- proc = run_publish(repo, "--patch")
278
+ proc = run_publish(repo, "--patch", "--dry-run")
317
279
  assert proc.returncode != 0
318
280
  assert "Consumer adoption" in proc.stderr
319
281
  assert read_version(repo) == "0.2.0"
320
282
  assert tags(repo) == []
283
+ assert last_subject(repo) == "chore: initial"
321
284
 
322
- def test_055_marker_without_adoption_section_fails(self, tmp_path):
323
- """Plan 055 migrates consumer repos onto profiles and recipes
324
- (profile key, trigger rewrite, isolation-block deletion), so its
325
- slug is registered: a release entry referencing it without a
326
- `### Consumer adoption` note gets refused."""
285
+ def test_dry_run_passes_violation_free_entry(self, tmp_path):
327
286
  repo, _ = make_repo(
328
287
  tmp_path,
329
- unreleased=(
330
- "### Changed\n\n"
331
- "- feat: implement plan 055-consumer-onboarding-init-doctor\n"
332
- ),
288
+ unreleased="### Changed\n\n- feat: add kernel sandbox\n",
333
289
  )
334
- proc = run_publish(repo, "--patch")
335
- assert proc.returncode != 0
336
- assert "Consumer adoption" in proc.stderr
290
+ proc = run_publish(repo, "--patch", "--dry-run")
291
+ assert proc.returncode == 0, proc.stderr
337
292
  assert read_version(repo) == "0.2.0"
338
- assert tags(repo) == []
339
-
340
293
 
341
294
  class TestGitOps:
342
295
  def test_commit_tag_and_push(self, tmp_path):