agentic-dev-loop 1.0.0a23__tar.gz → 1.0.0a25__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 (89) hide show
  1. {agentic_dev_loop-1.0.0a23/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a25}/PKG-INFO +137 -3
  2. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/README.md +136 -2
  3. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/core/config.py +36 -1
  5. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/agents.py +17 -2
  6. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/isolation.py +283 -12
  7. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/stages.py +6 -0
  8. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/sync_back.py +72 -8
  9. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25/src/agentic_dev_loop.egg-info}/PKG-INFO +137 -3
  10. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop.egg-info/SOURCES.txt +1 -0
  11. agentic_dev_loop-1.0.0a25/tests/test_029_merge_on_conflict.py +97 -0
  12. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_agents.py +26 -0
  13. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_isolation.py +431 -10
  14. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_isolation_integration.py +36 -3
  15. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_sdk_compat.py +18 -0
  16. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_stages.py +28 -0
  17. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_sync_back.py +127 -1
  18. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/LICENSE +0 -0
  19. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/setup.cfg +0 -0
  20. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/__init__.py +0 -0
  21. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/core/__init__.py +0 -0
  22. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/core/paths.py +0 -0
  23. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/core/results.py +0 -0
  24. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/core/verdict.py +0 -0
  25. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/__init__.py +0 -0
  26. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/actions.py +0 -0
  27. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/cli.py +0 -0
  28. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/deps.py +0 -0
  29. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  30. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  31. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  32. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  33. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  34. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/graph.py +0 -0
  35. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  36. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/llm.py +0 -0
  37. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/loaders.py +0 -0
  38. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/mounts.py +0 -0
  39. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/outputs.py +0 -0
  40. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/presets.py +0 -0
  41. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/proc.py +0 -0
  42. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/registry.py +0 -0
  43. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  44. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  45. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/secrets.py +0 -0
  46. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  47. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/py.typed +0 -0
  48. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/viz/__init__.py +0 -0
  49. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/viz/__main__.py +0 -0
  50. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop/viz/server.py +0 -0
  51. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  52. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  53. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  54. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  55. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_028_declared_caches.py +0 -0
  56. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_actions.py +0 -0
  57. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_aidlc.py +0 -0
  58. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_cli.py +0 -0
  59. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_config.py +0 -0
  60. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_deps.py +0 -0
  61. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_dry_run.py +0 -0
  62. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_e2e_fixtures.py +0 -0
  63. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_egress_proxy.py +0 -0
  64. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_graph.py +0 -0
  65. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_hostboundary.py +0 -0
  66. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_hostboundary_integration.py +0 -0
  67. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_isolation_pbt.py +0 -0
  68. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_llm.py +0 -0
  69. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_loaders.py +0 -0
  70. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_module_contract.py +0 -0
  71. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_outputs.py +0 -0
  72. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_outputs_pbt.py +0 -0
  73. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_proc.py +0 -0
  74. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_publish.py +0 -0
  75. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_qa_prompt.py +0 -0
  76. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_registry.py +0 -0
  77. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_repo_ascii.py +0 -0
  78. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_results.py +0 -0
  79. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_secrets.py +0 -0
  80. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_secrets_integration.py +0 -0
  81. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_secrets_pipeline.py +0 -0
  82. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_source_resolution.py +0 -0
  83. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_step_workspace.py +0 -0
  84. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_trigger_script.py +0 -0
  85. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_unit5_config.py +0 -0
  86. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_unit5_pbt.py +0 -0
  87. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_unit5_pure.py +0 -0
  88. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/tests/test_verdict.py +0 -0
  89. {agentic_dev_loop-1.0.0a23 → agentic_dev_loop-1.0.0a25}/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.0a23
3
+ Version: 1.0.0a25
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
@@ -79,8 +79,10 @@ flowchart LR
79
79
 
80
80
  The fixed policies *are* the product: fail fast on missing config/secrets, no
81
81
  silent fallbacks, verdicts only from artifacts, bounded retries, per-attempt
82
- artifact hygiene, a single clean-worktree-gated commit point, and no default
83
- LLM timeout (agent runs are long).
82
+ artifact hygiene, a single clean-worktree-gated commit point, no default LLM
83
+ timeout (agent runs are long), and fail fast on a broken sandbox - daemon-side
84
+ exec failures are classified and stop the run without an agent retry loop,
85
+ while a startup reaper reclaims containers leaked by a crashed run.
84
86
 
85
87
  ## Installation
86
88
 
@@ -221,6 +223,7 @@ steps:
221
223
  | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
222
224
  | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
223
225
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); `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). |
226
+ | `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
224
227
 
225
228
  ### Secrets (`secrets:`) - refs only, never values
226
229
 
@@ -305,6 +308,137 @@ steps:
305
308
  everywhere. Reserve it for stages that need it (QA, integration). The
306
309
  residual risks are documented in [docs/setup.md](docs/setup.md).
307
310
 
311
+ ### Sync-back conflicts (host edits during a run)
312
+
313
+ Only declared-`rw` paths sync back to the real repo after a step. If the host
314
+ also edits a declared-`rw` path while the step is running, sync-back prints
315
+ `[isolation] sync-back conflict: ...` and raises `AE-SBX-016`; the host's
316
+ version wins, the baseline is re-snapshotted, and the next step (even
317
+ `echo hello`) runs clean instead of re-raising forever. For append-style logs
318
+ edited by both sides (an audit log, say), declare them in the top-level
319
+ `merge_on_conflict:` list: those paths are union-merged - the host keeps its
320
+ lines, the sandbox's new lines are appended in order, both sides converge, and
321
+ the run continues with `[isolation] merged concurrent edits: ...` on the
322
+ console.
323
+
324
+
325
+ ## Authoring a loop for your own repo
326
+
327
+ A consumer repo needs two things written by hand - a pipeline file and the
328
+ agent files it names - both committed before the first run (the host-boundary
329
+ check refuses dirty tooling, and the `commit` step never commits it). Two
330
+ reference layouts ship today: this repo dogfoods the engine on itself
331
+ (python + pytest, no frontend, CLI-only qa), and the consumer repo carries
332
+ the wider shape (.NET + npm frontend + a networked browser-probing qa step).
333
+
334
+ ```
335
+ <repo>/agentic-dev-loop/pipeline.yaml
336
+ <repo>/agentic-dev-loop/sdk_agents/dev.agent.md
337
+ <repo>/agentic-dev-loop/sdk_agents/review.agent.md
338
+ <repo>/agentic-dev-loop/sdk_agents/qa.agent.md
339
+ ```
340
+
341
+ ### The agent files
342
+
343
+ Each `sdk_agents/<name>.agent.md` is a markdown prompt plus YAML frontmatter:
344
+ `name` matches a step's `agent:` value; `tools` lists the tool names the step
345
+ may use (`terminal` always; a repo can add more via extensions, e.g.
346
+ `figma`).
347
+
348
+ ```markdown
349
+ ---
350
+ name: review # matches a step's `agent:` value
351
+ tools: [terminal]
352
+ ---
353
+ # Review Agent
354
+ ## WORKFLOW # numbered steps
355
+ ## RULES # absolute never/always rules
356
+ ## SANDBOX NOTES # the sandbox truth for THIS step
357
+ ## OUTPUT # the verdict contract
358
+ ```
359
+
360
+ Three roles, three contracts:
361
+
362
+ | Role | Verdict contract | Typical sandbox |
363
+ | --- | --- | --- |
364
+ | dev | none - the final response is captured as the `produces:` summary | rw mount, offline |
365
+ | review | `VERDICT: APPROVED` / `VERDICT: NEEDS CHANGES` on the first content line | ro mount, offline |
366
+ | qa | `VERDICT: PASS` / `VERDICT: FAIL`; map `unverifiable: UNVERIFIABLE` if you want a third way | shaped by the repo: rw + network + `class: browser` when it boots servers and drives a browser (the consumer repo); ro + offline when it probes the CLI only (this repo) |
367
+
368
+ The conventions that make a good agent file - each one below exists because
369
+ an agent burned budget learning it mid-run:
370
+
371
+ - **State the sandbox truth in the prompt.** Every agent file carries a
372
+ `SANDBOX NOTES` section naming exactly what its step allows: mounts ro or
373
+ rw, network on or off, and which gates have already run. Never make the
374
+ agent discover it.
375
+ - **Gates are the source of truth - cite the record, never re-run.** A
376
+ command the test gate, the editor-tests gate, or the dev summary reports
377
+ as run is coverage evidence, cited as `covered by <test>` or the gate
378
+ record. Review and qa never re-run builds or suites; qa maps every
379
+ acceptance criterion to existing coverage and probes only the remainder.
380
+ - **dev enumerates its impossible set and stops at it.** The dev prompt
381
+ says: stop the moment your next step needs a command this sandbox cannot
382
+ run, and lists the set (package-manager fetches after a new pin, npm or
383
+ vitest offline, playwright downloads, database-category tests), one
384
+ attempt per command. That keeps the productive edit/build/test loop and
385
+ kills doomed retries. The set is toolchain-specific - write yours; this
386
+ repo's is a single bullet (no package managers), the consumer repo's is
387
+ four.
388
+ - **Failure output returns as *Previous attempt feedback*.** Steps route
389
+ `on_fail: { goto: dev, retry: true }`, so gate failure output lands in
390
+ the dev agent's next context; the dev prompt makes every listed issue a
391
+ required fix.
392
+ - **No git mutations, ever.** The `commit` step is the single commit point.
393
+ - **`max_iterations` per role.** qa is bounded probing (60 in both
394
+ reference repos); dev is open-ended construction and stays uncapped
395
+ (the engine default).
396
+
397
+ ### The pipeline file
398
+
399
+ The full field reference is above; this is the stage lineup and what to
400
+ adapt per repo.
401
+
402
+ | Stage | Kind | Typical shape | Why |
403
+ | --- | --- | --- | --- |
404
+ | dev | agent | rw, offline | does the work; edits are untracked |
405
+ | restore | command | network + `presets: [package-registries]` | fetches what dev pinned (`deps_policy: open`) or the locked install; also the audit escalation point |
406
+ | test | command | offline is fine (warm cache) | exit code is the verdict; the source of truth |
407
+ | editor-tests | command | network + `presets: [package-registries]` | frontend suites that cannot run offline (`npm ci && npm test`). A separate step, never merged into `test`: the step name is the failure diagnosis, its timeout does not contend the backend suite's, and a retry re-runs only its own chain |
408
+ | conformance | command | network + `presets: [package-registries]` | repo-specific fitness checks (the consumer repo: jscpd + DDD rules) |
409
+ | review | agent | ro, offline | static conformance verdict |
410
+ | qa | agent | per repo (see table above) | live-behaviour probes; `max_iterations: 60`; inject secrets only if the probes need them |
411
+ | commit | action | host-side | the single commit point |
412
+
413
+ Adaptation checklist for a new repo:
414
+
415
+ 1. `llm:` - model, `base_url`, `api_key_env`.
416
+ 2. `isolation:` - toolchain image override (the consumer repo pins a
417
+ toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
418
+ only if a stage runs Chromium.
419
+ 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
420
+ (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
421
+ `frontend-editor/node_modules`); caches are never synced back.
422
+ 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
423
+ 5. `secrets:` - refs only, resolved from host env; each step opts in to the
424
+ refs it needs.
425
+ 6. `egress:` - `presets: [package-registries]` for fetch steps;
426
+ `class: browser` (broad-but-logged) for browser-tier qa only.
427
+ 7. `deps_policy:` and `lockfiles:` - see
428
+ [Dependency policy](#dependency-policy-locked-by-default).
429
+ 8. The dev prompt's impossible set - write it for your toolchain.
430
+ 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
431
+ docs: shared context for every agent step.
432
+
433
+ Validate before the first run - dry-run loads and prints the resolved plan
434
+ with no LLM, no Docker, no state writes:
435
+
436
+ ```bash
437
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run
438
+ ```
439
+
440
+ Then commit the tooling and run for real.
441
+
308
442
  ## Dependency policy (locked by default)
309
443
 
310
444
  The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
@@ -37,8 +37,10 @@ flowchart LR
37
37
 
38
38
  The fixed policies *are* the product: fail fast on missing config/secrets, no
39
39
  silent fallbacks, verdicts only from artifacts, bounded retries, per-attempt
40
- artifact hygiene, a single clean-worktree-gated commit point, and no default
41
- LLM timeout (agent runs are long).
40
+ artifact hygiene, a single clean-worktree-gated commit point, no default LLM
41
+ timeout (agent runs are long), and fail fast on a broken sandbox - daemon-side
42
+ exec failures are classified and stop the run without an agent retry loop,
43
+ while a startup reaper reclaims containers leaked by a crashed run.
42
44
 
43
45
  ## Installation
44
46
 
@@ -179,6 +181,7 @@ steps:
179
181
  | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
180
182
  | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
181
183
  | `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); `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). |
184
+ | `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). |
182
185
 
183
186
  ### Secrets (`secrets:`) - refs only, never values
184
187
 
@@ -263,6 +266,137 @@ steps:
263
266
  everywhere. Reserve it for stages that need it (QA, integration). The
264
267
  residual risks are documented in [docs/setup.md](docs/setup.md).
265
268
 
269
+ ### Sync-back conflicts (host edits during a run)
270
+
271
+ Only declared-`rw` paths sync back to the real repo after a step. If the host
272
+ also edits a declared-`rw` path while the step is running, sync-back prints
273
+ `[isolation] sync-back conflict: ...` and raises `AE-SBX-016`; the host's
274
+ version wins, the baseline is re-snapshotted, and the next step (even
275
+ `echo hello`) runs clean instead of re-raising forever. For append-style logs
276
+ edited by both sides (an audit log, say), declare them in the top-level
277
+ `merge_on_conflict:` list: those paths are union-merged - the host keeps its
278
+ lines, the sandbox's new lines are appended in order, both sides converge, and
279
+ the run continues with `[isolation] merged concurrent edits: ...` on the
280
+ console.
281
+
282
+
283
+ ## Authoring a loop for your own repo
284
+
285
+ A consumer repo needs two things written by hand - a pipeline file and the
286
+ agent files it names - both committed before the first run (the host-boundary
287
+ check refuses dirty tooling, and the `commit` step never commits it). Two
288
+ reference layouts ship today: this repo dogfoods the engine on itself
289
+ (python + pytest, no frontend, CLI-only qa), and the consumer repo carries
290
+ the wider shape (.NET + npm frontend + a networked browser-probing qa step).
291
+
292
+ ```
293
+ <repo>/agentic-dev-loop/pipeline.yaml
294
+ <repo>/agentic-dev-loop/sdk_agents/dev.agent.md
295
+ <repo>/agentic-dev-loop/sdk_agents/review.agent.md
296
+ <repo>/agentic-dev-loop/sdk_agents/qa.agent.md
297
+ ```
298
+
299
+ ### The agent files
300
+
301
+ Each `sdk_agents/<name>.agent.md` is a markdown prompt plus YAML frontmatter:
302
+ `name` matches a step's `agent:` value; `tools` lists the tool names the step
303
+ may use (`terminal` always; a repo can add more via extensions, e.g.
304
+ `figma`).
305
+
306
+ ```markdown
307
+ ---
308
+ name: review # matches a step's `agent:` value
309
+ tools: [terminal]
310
+ ---
311
+ # Review Agent
312
+ ## WORKFLOW # numbered steps
313
+ ## RULES # absolute never/always rules
314
+ ## SANDBOX NOTES # the sandbox truth for THIS step
315
+ ## OUTPUT # the verdict contract
316
+ ```
317
+
318
+ Three roles, three contracts:
319
+
320
+ | Role | Verdict contract | Typical sandbox |
321
+ | --- | --- | --- |
322
+ | dev | none - the final response is captured as the `produces:` summary | rw mount, offline |
323
+ | review | `VERDICT: APPROVED` / `VERDICT: NEEDS CHANGES` on the first content line | ro mount, offline |
324
+ | qa | `VERDICT: PASS` / `VERDICT: FAIL`; map `unverifiable: UNVERIFIABLE` if you want a third way | shaped by the repo: rw + network + `class: browser` when it boots servers and drives a browser (the consumer repo); ro + offline when it probes the CLI only (this repo) |
325
+
326
+ The conventions that make a good agent file - each one below exists because
327
+ an agent burned budget learning it mid-run:
328
+
329
+ - **State the sandbox truth in the prompt.** Every agent file carries a
330
+ `SANDBOX NOTES` section naming exactly what its step allows: mounts ro or
331
+ rw, network on or off, and which gates have already run. Never make the
332
+ agent discover it.
333
+ - **Gates are the source of truth - cite the record, never re-run.** A
334
+ command the test gate, the editor-tests gate, or the dev summary reports
335
+ as run is coverage evidence, cited as `covered by <test>` or the gate
336
+ record. Review and qa never re-run builds or suites; qa maps every
337
+ acceptance criterion to existing coverage and probes only the remainder.
338
+ - **dev enumerates its impossible set and stops at it.** The dev prompt
339
+ says: stop the moment your next step needs a command this sandbox cannot
340
+ run, and lists the set (package-manager fetches after a new pin, npm or
341
+ vitest offline, playwright downloads, database-category tests), one
342
+ attempt per command. That keeps the productive edit/build/test loop and
343
+ kills doomed retries. The set is toolchain-specific - write yours; this
344
+ repo's is a single bullet (no package managers), the consumer repo's is
345
+ four.
346
+ - **Failure output returns as *Previous attempt feedback*.** Steps route
347
+ `on_fail: { goto: dev, retry: true }`, so gate failure output lands in
348
+ the dev agent's next context; the dev prompt makes every listed issue a
349
+ required fix.
350
+ - **No git mutations, ever.** The `commit` step is the single commit point.
351
+ - **`max_iterations` per role.** qa is bounded probing (60 in both
352
+ reference repos); dev is open-ended construction and stays uncapped
353
+ (the engine default).
354
+
355
+ ### The pipeline file
356
+
357
+ The full field reference is above; this is the stage lineup and what to
358
+ adapt per repo.
359
+
360
+ | Stage | Kind | Typical shape | Why |
361
+ | --- | --- | --- | --- |
362
+ | dev | agent | rw, offline | does the work; edits are untracked |
363
+ | restore | command | network + `presets: [package-registries]` | fetches what dev pinned (`deps_policy: open`) or the locked install; also the audit escalation point |
364
+ | test | command | offline is fine (warm cache) | exit code is the verdict; the source of truth |
365
+ | editor-tests | command | network + `presets: [package-registries]` | frontend suites that cannot run offline (`npm ci && npm test`). A separate step, never merged into `test`: the step name is the failure diagnosis, its timeout does not contend the backend suite's, and a retry re-runs only its own chain |
366
+ | conformance | command | network + `presets: [package-registries]` | repo-specific fitness checks (the consumer repo: jscpd + DDD rules) |
367
+ | review | agent | ro, offline | static conformance verdict |
368
+ | qa | agent | per repo (see table above) | live-behaviour probes; `max_iterations: 60`; inject secrets only if the probes need them |
369
+ | commit | action | host-side | the single commit point |
370
+
371
+ Adaptation checklist for a new repo:
372
+
373
+ 1. `llm:` - model, `base_url`, `api_key_env`.
374
+ 2. `isolation:` - toolchain image override (the consumer repo pins a
375
+ toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
376
+ only if a stage runs Chromium.
377
+ 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
378
+ (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
379
+ `frontend-editor/node_modules`); caches are never synced back.
380
+ 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
381
+ 5. `secrets:` - refs only, resolved from host env; each step opts in to the
382
+ refs it needs.
383
+ 6. `egress:` - `presets: [package-registries]` for fetch steps;
384
+ `class: browser` (broad-but-logged) for browser-tier qa only.
385
+ 7. `deps_policy:` and `lockfiles:` - see
386
+ [Dependency policy](#dependency-policy-locked-by-default).
387
+ 8. The dev prompt's impossible set - write it for your toolchain.
388
+ 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
389
+ docs: shared context for every agent step.
390
+
391
+ Validate before the first run - dry-run loads and prints the resolved plan
392
+ with no LLM, no Docker, no state writes:
393
+
394
+ ```bash
395
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run
396
+ ```
397
+
398
+ Then commit the tooling and run for real.
399
+
266
400
  ## Dependency policy (locked by default)
267
401
 
268
402
  The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a23"
7
+ version = "1.0.0a25"
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"
@@ -446,6 +446,10 @@ class PipelineConfig(BaseModel):
446
446
  # "no sync-back excludes".
447
447
  caches: list[str] = []
448
448
  sync_excludes: list[str] = []
449
+ # 029: append-style logs that sync-back union-merges on a concurrent
450
+ # edit instead of raising. Same declared-path vocabulary as
451
+ # ``sync_excludes:``.
452
+ merge_on_conflict: list[str] = []
449
453
 
450
454
  @field_validator("deps_policy")
451
455
  @classmethod
@@ -460,7 +464,7 @@ class PipelineConfig(BaseModel):
460
464
  def _coerce_lockfiles(cls, v: Any) -> Any:
461
465
  return [v] if isinstance(v, str) else v
462
466
 
463
- @field_validator("caches", "sync_excludes", mode="before")
467
+ @field_validator("caches", "sync_excludes", "merge_on_conflict", mode="before")
464
468
  @classmethod
465
469
  def _coerce_declared_paths(cls, v: Any) -> Any:
466
470
  return [v] if isinstance(v, str) else v
@@ -475,6 +479,11 @@ class PipelineConfig(BaseModel):
475
479
  def _valid_sync_excludes(cls, v: list[str]) -> list[str]:
476
480
  return [_validate_declared_path(c, allow_wildcards=True) for c in v]
477
481
 
482
+ @field_validator("merge_on_conflict")
483
+ @classmethod
484
+ def _valid_merge_on_conflict(cls, v: list[str]) -> list[str]:
485
+ return [_validate_declared_path(m, allow_wildcards=True) for m in v]
486
+
478
487
  @field_validator("secrets", mode="before")
479
488
  @classmethod
480
489
  def _reject_value_shaped_refs(cls, v: Any) -> Any:
@@ -579,6 +588,32 @@ class PipelineConfig(BaseModel):
579
588
  f"{('/'.join(cp))!r}")
580
589
  return self
581
590
 
591
+ @model_validator(mode="after")
592
+ def _merge_avoid_excludes_and_caches(self) -> "PipelineConfig":
593
+ """029: a literal merge_on_conflict entry equal to or under a literal
594
+ sync_excludes entry or a literal caches entry is redundant - excluded
595
+ and cached paths never sync, so they can never conflict. Wildcard
596
+ merge entries are skipped (their overlap is fuzzy, 028 parity)."""
597
+ cache_parts = [relative_parts(c) for c in self.caches if relative_parts(c)]
598
+ exclude_parts = [relative_parts(e) for e in self.sync_excludes
599
+ if not _has_wildcards(e) and relative_parts(e)]
600
+ for merge in self.merge_on_conflict:
601
+ if _has_wildcards(merge):
602
+ continue
603
+ mp = relative_parts(merge)
604
+ for cp in cache_parts:
605
+ if mp[:len(cp)] == cp:
606
+ raise ValueError(
607
+ f"merge_on_conflict entry {merge!r} is redundant: it is "
608
+ f"already excluded as declared cache {('/'.join(cp))!r}")
609
+ for ep in exclude_parts:
610
+ if mp[:len(ep)] == ep:
611
+ raise ValueError(
612
+ f"merge_on_conflict entry {merge!r} is redundant: it is "
613
+ f"already excluded by sync_excludes entry "
614
+ f"{('/'.join(ep))!r}")
615
+ return self
616
+
582
617
  @model_validator(mode="after")
583
618
  def _sandbox_venv_not_declared_cache(self) -> "PipelineConfig":
584
619
  """028: the sandbox venv is engine infrastructure, never author data.
@@ -15,7 +15,8 @@ from openhands.tools.file_editor import FileEditorTool # noqa: F401 - registers
15
15
  from openhands.tools.task_tracker import TaskTrackerTool # noqa: F401 - registers "task_tracker"
16
16
  from openhands.tools.terminal import TerminalTool # noqa: F401 - registers "terminal"
17
17
 
18
- from .isolation import SandboxExecutionRequest, command_argv, resolve_sandbox_env
18
+ from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
19
+ resolve_sandbox_env)
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from openhands.sdk.agent.agent import Agent
@@ -96,7 +97,21 @@ class SandboxTerminalExecutor(ToolExecutor):
96
97
  env=self._env, timeout=action.timeout,
97
98
  pipeline=self._pipeline, step=self._step, network=self._network,
98
99
  mount_set=self._mount_set, egress_policy=self._egress_policy)
99
- result = self._sandbox.run(request)
100
+ try:
101
+ result = self._sandbox.run(request)
102
+ except SandboxError as exc:
103
+ # 030, D3: a sticky infra failure proves the exec path is dead, so
104
+ # tell the SDK run loop to break this step instead of flapping to
105
+ # max_iterations. See COMPAT-SHIMS.md for the reliance + canary.
106
+ if (conversation is not None
107
+ and exc is self._sandbox.infra_error):
108
+ from openhands.sdk.conversation.state import (
109
+ ConversationExecutionStatus,
110
+ )
111
+ conversation.state.execution_status = (
112
+ ConversationExecutionStatus.STUCK
113
+ )
114
+ raise
100
115
  output = result.stdout + result.stderr
101
116
  return TerminalObservation.from_text(
102
117
  output, is_error=not result.succeeded and not result.is_timeout,