agentic-dev-loop 0.10.0__tar.gz → 0.10.2__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.0/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.10.2}/PKG-INFO +40 -13
  2. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/README.md +39 -12
  3. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/pyproject.toml +3 -1
  4. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/config.py +19 -0
  5. agentic_dev_loop-0.10.2/src/agentic_dev_loop/core/profiles/__init__.py +178 -0
  6. agentic_dev_loop-0.10.2/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +22 -0
  7. agentic_dev_loop-0.10.2/src/agentic_dev_loop/core/profiles/data/python.yaml +8 -0
  8. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/cli.py +6 -0
  9. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/hostboundary.py +3 -0
  10. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/__init__.py +6 -0
  11. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/checks.py +272 -0
  12. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/doctor.py +84 -0
  13. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/init.py +432 -0
  14. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +85 -0
  15. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +57 -0
  16. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/recipes.py +77 -0
  17. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +133 -0
  18. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/env.example +11 -0
  19. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +181 -0
  20. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +53 -0
  21. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +97 -0
  22. agentic_dev_loop-0.10.2/src/agentic_dev_loop/engine/onboarding/templates.py +62 -0
  23. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2/src/agentic_dev_loop.egg-info}/PKG-INFO +40 -13
  24. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/SOURCES.txt +24 -0
  25. agentic_dev_loop-0.10.2/tests/test_055_cli.py +51 -0
  26. agentic_dev_loop-0.10.2/tests/test_055_doctor.py +163 -0
  27. agentic_dev_loop-0.10.2/tests/test_055_init.py +135 -0
  28. agentic_dev_loop-0.10.2/tests/test_055_profile_loading.py +73 -0
  29. agentic_dev_loop-0.10.2/tests/test_055_profiles.py +156 -0
  30. agentic_dev_loop-0.10.2/tests/test_055_recipes.py +84 -0
  31. agentic_dev_loop-0.10.2/tests/test_055_schema_lock.py +42 -0
  32. agentic_dev_loop-0.10.2/tests/test_055_templates.py +80 -0
  33. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_hostboundary.py +10 -0
  34. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_module_contract.py +20 -1
  35. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_publish.py +38 -0
  36. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_trigger_script.py +84 -32
  37. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_viz.py +1 -1
  38. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/LICENSE +0 -0
  39. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/setup.cfg +0 -0
  40. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/__init__.py +0 -0
  41. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/__init__.py +0 -0
  42. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/paths.py +0 -0
  43. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/results.py +0 -0
  44. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/core/verdict.py +0 -0
  45. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/__init__.py +0 -0
  46. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/actions.py +0 -0
  47. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  48. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  49. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/agents.py +0 -0
  50. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  51. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  52. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  53. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  54. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/containers.py +0 -0
  55. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/deps.py +0 -0
  56. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  57. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  58. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  59. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  60. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  61. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  62. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/graph.py +0 -0
  63. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/isolation.py +0 -0
  64. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/llm.py +0 -0
  65. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/loaders.py +0 -0
  66. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/mounts.py +0 -0
  67. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/outputs.py +0 -0
  68. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/presets.py +0 -0
  69. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/proc.py +0 -0
  70. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/registry.py +0 -0
  71. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  72. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  73. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/secrets.py +0 -0
  74. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/stages.py +0 -0
  75. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  76. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  77. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/py.typed +0 -0
  78. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/viz/__init__.py +0 -0
  79. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/viz/__main__.py +0 -0
  80. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop/viz/server.py +0 -0
  81. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  82. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  83. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  84. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  85. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_029_merge_on_conflict.py +0 -0
  86. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_033_brief_step.py +0 -0
  87. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_035_finalize_nudge.py +0 -0
  88. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  89. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_041_brief_coverage.py +0 -0
  90. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_046_brief_prompt.py +0 -0
  91. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_actions.py +0 -0
  92. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_agent_error_guide.py +0 -0
  93. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_agents.py +0 -0
  94. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_aidlc.py +0 -0
  95. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_cli.py +0 -0
  96. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_config.py +0 -0
  97. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_containers.py +0 -0
  98. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_deps.py +0 -0
  99. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_dry_run.py +0 -0
  100. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_e2e_fixtures.py +0 -0
  101. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_egress_proxy.py +0 -0
  102. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_graph.py +0 -0
  103. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_hostboundary_integration.py +0 -0
  104. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_isolation.py +0 -0
  105. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_isolation_integration.py +0 -0
  106. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_isolation_pbt.py +0 -0
  107. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_llm.py +0 -0
  108. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_loaders.py +0 -0
  109. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_outputs.py +0 -0
  110. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_outputs_pbt.py +0 -0
  111. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_proc.py +0 -0
  112. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_qa_prompt.py +0 -0
  113. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_registry.py +0 -0
  114. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_repo_ascii.py +0 -0
  115. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_results.py +0 -0
  116. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_sdk_compat.py +0 -0
  117. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_secrets.py +0 -0
  118. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_secrets_integration.py +0 -0
  119. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_secrets_pipeline.py +0 -0
  120. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_source_resolution.py +0 -0
  121. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_stages.py +0 -0
  122. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_step_workspace.py +0 -0
  123. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_sync_back.py +0 -0
  124. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_unit5_config.py +0 -0
  125. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_unit5_pbt.py +0 -0
  126. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_unit5_pure.py +0 -0
  127. {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.2}/tests/test_verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.10.0
3
+ Version: 0.10.2
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
@@ -275,6 +275,7 @@ steps:
275
275
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
276
276
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
277
277
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
278
+ | `profile:` (top-level) | Names a bundled toolchain profile (`python`, `dotnet-node`) or a consumer profile file path. `load_config` merges the profile at load time: isolation key-wise with the pipeline winning, caches unioned profile-first. Absent = today's behavior. |
278
279
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
279
280
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
280
281
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
@@ -446,12 +447,39 @@ console.
446
447
 
447
448
  ## Authoring a loop for your own repo
448
449
 
449
- A consumer repo needs two things written by hand - a pipeline file and the
450
- agent files it names - both committed before the first run (the host-boundary
451
- check refuses dirty tooling, and the `commit` step never commits it). Two
452
- reference layouts ship today: this repo dogfoods the engine on itself
453
- (python + pytest, no frontend, CLI-only qa), and the consumer repo carries
454
- the wider shape (.NET + npm frontend + a networked browser-probing qa step).
450
+ ### Start with `agentic-dev-loop init`
451
+
452
+ `init` writes the onboarding surface from a bundled toolchain profile:
453
+
454
+ ```bash
455
+ agentic-dev-loop init --profile python # or: --profile dotnet-node
456
+ ```
457
+
458
+ It detects solution files, `package.json` directories, lockfiles, README, and
459
+ AGENTS.md, asks only what detection cannot answer, and writes
460
+ `agentic-dev-loop/pipeline.yaml`, the three agent files, an executable
461
+ `trigger-agentic-dev-loop.sh`, `agentic-dev-loop/.engine-version` (the exact
462
+ installed engine version), any missing premise files, gitignore additions, and
463
+ an env-file template. It refuses to overwrite an existing
464
+ `agentic-dev-loop/` directory. A consumer-authored profile file can be passed
465
+ by path instead of a bundled name.
466
+
467
+ Profiles carry the environment facts every app on a toolchain shares (image,
468
+ cpus, caches, premise files, gitignore lines); recipes carry the gate shape and
469
+ command vocabulary. The pipeline references a profile with the top-level
470
+ `profile:` key, and `load_config` merges it at load time: the pipeline wins on
471
+ scalars, caches union profile-first, and absence of the key behaves exactly as
472
+ before.
473
+
474
+ `agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
475
+ Hard failures abort the run (engine pin mismatch, engine runtime availability,
476
+ API version, git repo, `.env*` files); warnings print and continue. `doctor
477
+ --json` emits the same results mechanically. The trigger repairs a pin/install
478
+ mismatch exactly once, then fails hard.
479
+
480
+ A consumer repo needs a pipeline file and the agent files it names, both
481
+ committed before the first run (the host-boundary check refuses dirty tooling,
482
+ and the `commit` step never commits it). `init` writes them; the layout is:
455
483
 
456
484
  ```
457
485
  <repo>/agentic-dev-loop/pipeline.yaml
@@ -537,12 +565,11 @@ Adaptation checklist for a new repo:
537
565
 
538
566
  1. `llm:` - model, `base_url`, `api_key_env`.
539
567
  2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
540
- 3. `isolation:` - toolchain image override (the consumer repo pins a
541
- toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
542
- only if a stage runs Chromium.
543
- 4. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
544
- (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
545
- `frontend-editor/node_modules`); caches are never synced back, and a
568
+ 3. `profile:` - name a bundled profile (`python`, `dotnet-node`) or a
569
+ consumer profile path. The profile supplies `isolation:` (image,
570
+ `seccomp_profile`, cpus) and `caches:`; pipeline keys override its scalars.
571
+ 4. `caches:` - profile-supplied toolchain caches plus any run-specific dirs
572
+ (`.nuget-packages`, `qa/.browsers`); caches are never synced back, and a
546
573
  step's `setup:` hook runs with them mounted so it can warm them.
547
574
  5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
548
575
  6. `secrets:` - refs only, resolved from host env; each step opts in to the
@@ -231,6 +231,7 @@ steps:
231
231
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
232
232
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
233
233
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
234
+ | `profile:` (top-level) | Names a bundled toolchain profile (`python`, `dotnet-node`) or a consumer profile file path. `load_config` merges the profile at load time: isolation key-wise with the pipeline winning, caches unioned profile-first. Absent = today's behavior. |
234
235
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
235
236
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
236
237
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
@@ -402,12 +403,39 @@ console.
402
403
 
403
404
  ## Authoring a loop for your own repo
404
405
 
405
- A consumer repo needs two things written by hand - a pipeline file and the
406
- agent files it names - both committed before the first run (the host-boundary
407
- check refuses dirty tooling, and the `commit` step never commits it). Two
408
- reference layouts ship today: this repo dogfoods the engine on itself
409
- (python + pytest, no frontend, CLI-only qa), and the consumer repo carries
410
- the wider shape (.NET + npm frontend + a networked browser-probing qa step).
406
+ ### Start with `agentic-dev-loop init`
407
+
408
+ `init` writes the onboarding surface from a bundled toolchain profile:
409
+
410
+ ```bash
411
+ agentic-dev-loop init --profile python # or: --profile dotnet-node
412
+ ```
413
+
414
+ It detects solution files, `package.json` directories, lockfiles, README, and
415
+ AGENTS.md, asks only what detection cannot answer, and writes
416
+ `agentic-dev-loop/pipeline.yaml`, the three agent files, an executable
417
+ `trigger-agentic-dev-loop.sh`, `agentic-dev-loop/.engine-version` (the exact
418
+ installed engine version), any missing premise files, gitignore additions, and
419
+ an env-file template. It refuses to overwrite an existing
420
+ `agentic-dev-loop/` directory. A consumer-authored profile file can be passed
421
+ by path instead of a bundled name.
422
+
423
+ Profiles carry the environment facts every app on a toolchain shares (image,
424
+ cpus, caches, premise files, gitignore lines); recipes carry the gate shape and
425
+ command vocabulary. The pipeline references a profile with the top-level
426
+ `profile:` key, and `load_config` merges it at load time: the pipeline wins on
427
+ scalars, caches union profile-first, and absence of the key behaves exactly as
428
+ before.
429
+
430
+ `agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
431
+ Hard failures abort the run (engine pin mismatch, engine runtime availability,
432
+ API version, git repo, `.env*` files); warnings print and continue. `doctor
433
+ --json` emits the same results mechanically. The trigger repairs a pin/install
434
+ mismatch exactly once, then fails hard.
435
+
436
+ A consumer repo needs a pipeline file and the agent files it names, both
437
+ committed before the first run (the host-boundary check refuses dirty tooling,
438
+ and the `commit` step never commits it). `init` writes them; the layout is:
411
439
 
412
440
  ```
413
441
  <repo>/agentic-dev-loop/pipeline.yaml
@@ -493,12 +521,11 @@ Adaptation checklist for a new repo:
493
521
 
494
522
  1. `llm:` - model, `base_url`, `api_key_env`.
495
523
  2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
496
- 3. `isolation:` - toolchain image override (the consumer repo pins a
497
- toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
498
- only if a stage runs Chromium.
499
- 4. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
500
- (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
501
- `frontend-editor/node_modules`); caches are never synced back, and a
524
+ 3. `profile:` - name a bundled profile (`python`, `dotnet-node`) or a
525
+ consumer profile path. The profile supplies `isolation:` (image,
526
+ `seccomp_profile`, cpus) and `caches:`; pipeline keys override its scalars.
527
+ 4. `caches:` - profile-supplied toolchain caches plus any run-specific dirs
528
+ (`.nuget-packages`, `qa/.browsers`); caches are never synced back, and a
502
529
  step's `setup:` hook runs with them mounted so it can warm them.
503
530
  5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
504
531
  6. `secrets:` - refs only, resolved from host env; each step opts in to the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "0.10.0"
7
+ version = "0.10.2"
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"
@@ -76,8 +76,10 @@ where = ["src"]
76
76
 
77
77
  [tool.setuptools.package-data]
78
78
  agentic_dev_loop = ["py.typed"]
79
+ "agentic_dev_loop.core.profiles" = ["data/*.yaml"]
79
80
  "agentic_dev_loop.engine" = ["*.md"]
80
81
  "agentic_dev_loop.engine.seccomp" = ["*.json"]
82
+ "agentic_dev_loop.engine.onboarding" = ["recipes/*.yaml", "templates/*"]
81
83
 
82
84
  [tool.pytest.ini_options]
83
85
  markers = [
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
+ import importlib.metadata
6
7
  import re
7
8
  import warnings
8
9
  from pathlib import Path
@@ -647,6 +648,9 @@ class PipelineConfig(BaseModel):
647
648
  # edit instead of raising. Same declared-path vocabulary as
648
649
  # ``sync_excludes:``.
649
650
  merge_on_conflict: list[str] = []
651
+ # 055: the toolchain profile resolved at load time. Kept on the validated
652
+ # config so the run records which profile supplied its defaults.
653
+ profile: str | None = None
650
654
 
651
655
  @field_validator("deps_policy")
652
656
  @classmethod
@@ -892,6 +896,21 @@ def load_config(path: Path) -> PipelineConfig:
892
896
  raise ValueError(
893
897
  f"pipeline config must be a mapping, got {type(data).__name__}: {path}"
894
898
  )
899
+ profile_ref = data.pop("profile", None)
900
+ if profile_ref is not None:
901
+ from agentic_dev_loop.core.profiles import (
902
+ ProfileError,
903
+ load_profile,
904
+ merge_profile,
905
+ )
906
+
907
+ try:
908
+ installed = importlib.metadata.version("agentic-dev-loop")
909
+ profile = load_profile(str(profile_ref), installed)
910
+ data = merge_profile(data, profile)
911
+ except ProfileError as exc:
912
+ raise ValueError(f"pipeline {path}: {exc}") from exc
913
+ data["profile"] = profile_ref
895
914
  if "state_dir" not in data:
896
915
  data["state_dir"] = default_state_dir(path)
897
916
  cfg = PipelineConfig.model_validate(data)
@@ -0,0 +1,178 @@
1
+ """Toolchain profiles: bundled data plus the load-time merge into a pipeline.
2
+
3
+ A profile supplies the environment facts every app on a toolchain shares
4
+ (isolation image, caches, premise files, gitignore lines, doctor probes).
5
+ Profiles never carry commands, gates, or steps - those live in
6
+ engine-resident recipes. The subpackage is intentionally dependency-light
7
+ (stdlib + pydantic + PyYAML) so ``core`` can resolve a profile at config
8
+ load time without importing the engine.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from importlib import resources
15
+ from pathlib import Path
16
+
17
+ import yaml
18
+ from pydantic import BaseModel, ConfigDict, Field
19
+
20
+ __all__ = [
21
+ "PremiseFile",
22
+ "Profile",
23
+ "ProfileError",
24
+ "ProfileIsolation",
25
+ "bundled_profile_names",
26
+ "check_engine_min",
27
+ "load_profile",
28
+ "merge_profile",
29
+ "parse_version",
30
+ ]
31
+
32
+ _VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)(\d+))?$")
33
+ _RANK = {"a": 0, "b": 1, "rc": 2}
34
+ _FINAL = 3
35
+
36
+
37
+ class ProfileError(ValueError):
38
+ """Any profile load, parse, or compatibility failure."""
39
+
40
+
41
+ class PremiseFile(BaseModel):
42
+ model_config = ConfigDict(extra="forbid")
43
+
44
+ path: str
45
+ content: str
46
+
47
+
48
+ class ProfileIsolation(BaseModel):
49
+ model_config = ConfigDict(extra="forbid")
50
+
51
+ image: str
52
+ seccomp_profile: str | None = None
53
+ cpus: int
54
+
55
+
56
+ class Profile(BaseModel):
57
+ model_config = ConfigDict(extra="forbid")
58
+
59
+ name: str
60
+ engine_min: str
61
+ description: str = ""
62
+ isolation: ProfileIsolation
63
+ caches: list[str] = Field(default_factory=list)
64
+ premise_files: list[PremiseFile] = Field(default_factory=list)
65
+ gitignore_additions: list[str] = Field(default_factory=list)
66
+ doctor_checks: list = Field(default_factory=list)
67
+
68
+
69
+ def parse_version(text: str) -> tuple[int, int, int, int, int]:
70
+ """Parse ``X.Y.Z[a|b|rcN]`` into a directly comparable 5-tuple.
71
+
72
+ Rank ordering is alpha < beta < rc < final; the prerelease number is 0
73
+ for a final release so ``1.0.0rc9 < 1.0.0``.
74
+ """
75
+ match = _VERSION_RE.match(text.strip())
76
+ if match is None:
77
+ raise ProfileError(f"unparseable version: {text!r}")
78
+ major, minor, patch, pre, num = match.groups()
79
+ if pre is None:
80
+ return (int(major), int(minor), int(patch), _FINAL, 0)
81
+ return (int(major), int(minor), int(patch), _RANK[pre], int(num))
82
+
83
+
84
+ def check_engine_min(engine_min: str, installed: str,
85
+ latest: str | None = None) -> None:
86
+ """Raise unless ``installed`` satisfies ``engine_min``.
87
+
88
+ ``latest`` is substituted into the fix line by the caller (doctor); when
89
+ omitted the floor itself is used.
90
+ """
91
+ if parse_version(installed) < parse_version(engine_min):
92
+ target = latest or engine_min
93
+ raise ProfileError(
94
+ f"profile requires engine >= {engine_min}; installed {installed}. "
95
+ f'Upgrade: pipx install --force "agentic-dev-loop=={target}"')
96
+
97
+
98
+ def _data_dir():
99
+ return resources.files(__package__).joinpath("data")
100
+
101
+
102
+ def bundled_profile_names() -> list[str]:
103
+ return sorted(
104
+ entry.name[: -len(".yaml")]
105
+ for entry in _data_dir().iterdir()
106
+ if entry.name.endswith(".yaml")
107
+ )
108
+
109
+
110
+ def _read_yaml(text: str, source: str) -> dict:
111
+ try:
112
+ data = yaml.safe_load(text)
113
+ except yaml.YAMLError as exc:
114
+ raise ProfileError(f"profile {source!r}: invalid YAML: {exc}") from exc
115
+ if not isinstance(data, dict):
116
+ raise ProfileError(f"profile {source!r} must be a mapping")
117
+ return data
118
+
119
+
120
+ def _validate(data: dict, source: str) -> Profile:
121
+ try:
122
+ return Profile.model_validate(data)
123
+ except Exception as exc:
124
+ raise ProfileError(f"profile {source!r}: {exc}") from exc
125
+
126
+
127
+ def load_profile(reference: str, installed_version: str) -> Profile:
128
+ """Load a bundled profile by name or a consumer profile by path.
129
+
130
+ Existence decides: a filesystem path that exists is read as a consumer
131
+ profile; anything else must name a bundled profile.
132
+ """
133
+ candidate = Path(reference)
134
+ if candidate.is_file():
135
+ data = _read_yaml(candidate.read_text(encoding="utf-8"), reference)
136
+ profile = _validate(data, reference)
137
+ else:
138
+ names = bundled_profile_names()
139
+ if reference not in names:
140
+ raise ProfileError(
141
+ f"unknown profile '{reference}'; bundled profiles: "
142
+ + ", ".join(names))
143
+ entry = _data_dir().joinpath(f"{reference}.yaml")
144
+ data = _read_yaml(entry.read_text(encoding="utf-8"), reference)
145
+ if data.get("name") != reference:
146
+ raise ProfileError(
147
+ f"profile {reference!r}: declared name {data.get('name')!r} "
148
+ f"does not match its filename")
149
+ profile = _validate(data, reference)
150
+ check_engine_min(profile.engine_min, installed_version)
151
+ return profile
152
+
153
+
154
+ def _dedupe(items: list[str]) -> list[str]:
155
+ seen: set[str] = set()
156
+ out: list[str] = []
157
+ for item in items:
158
+ if item not in seen:
159
+ seen.add(item)
160
+ out.append(item)
161
+ return out
162
+
163
+
164
+ def merge_profile(data: dict, profile: Profile) -> dict:
165
+ """Merge a profile into a pipeline dict; the pipeline wins.
166
+
167
+ Isolation merges key-wise (profile under, pipeline over). Caches union
168
+ profile-first, dedupe-preserving-order. Every other key is untouched.
169
+ """
170
+ merged = dict(data)
171
+ profile_iso = profile.isolation.model_dump(exclude_none=True)
172
+ pipeline_iso = merged.get("isolation") or {}
173
+ if not isinstance(pipeline_iso, dict):
174
+ pipeline_iso = {}
175
+ merged["isolation"] = {**profile_iso, **pipeline_iso}
176
+ caches = _dedupe(list(profile.caches) + list(merged.get("caches") or []))
177
+ merged["caches"] = caches
178
+ return merged
@@ -0,0 +1,22 @@
1
+ name: dotnet-node
2
+ engine_min: 0.9.2
3
+ description: .NET backend with a Node.js frontend
4
+ isolation:
5
+ image: ghcr.io/lukedawilson/agentic-dev-loop/dev-dotnet@sha256:d15d1c6115fafc3d5f3bfb567338d61441081e5d7ff83d7770bc0b5f14f977fc
6
+ seccomp_profile: bundled:chromium-userns
7
+ cpus: 6
8
+ caches:
9
+ - .nuget-packages
10
+ premise_files:
11
+ - path: NuGet.Config
12
+ content: |
13
+ <configuration>
14
+ <config>
15
+ <add key="globalPackagesFolder" value=".nuget-packages" />
16
+ </config>
17
+ </configuration>
18
+ gitignore_additions:
19
+ - bin/
20
+ - obj/
21
+ - node_modules/
22
+ - .nuget-packages/
@@ -0,0 +1,8 @@
1
+ name: python
2
+ engine_min: 0.9.2
3
+ description: Python project; the sandbox builds its own venv per run
4
+ isolation:
5
+ image: ghcr.io/lukedawilson/agentic-dev-loop/dev@sha256:890105209c127446caf20ce3dc05f0c0f5527e9d679e18cc0075e6b0ee917434
6
+ cpus: 2
7
+ gitignore_additions:
8
+ - .venv-sandbox/
@@ -165,6 +165,12 @@ def main(argv: list[str] | None = None) -> int:
165
165
  argv = list(argv)
166
166
  if argv and argv[0] == "containers":
167
167
  return _run_containers(argv[1:])
168
+ if argv and argv[0] == "init":
169
+ from .onboarding.init import run_init
170
+ return run_init(argv[1:])
171
+ if argv and argv[0] == "doctor":
172
+ from .onboarding.doctor import run_doctor
173
+ return run_doctor(argv[1:])
168
174
  probe = argparse.ArgumentParser(add_help=False)
169
175
  probe.add_argument("pipeline", type=Path, nargs="?")
170
176
  _add_version_flag(probe)
@@ -32,6 +32,8 @@ if TYPE_CHECKING:
32
32
  # the dogfood pipeline and the consumer repo use); the other tooling paths are config- or
33
33
  # pipeline-file-relative.
34
34
  TOOLING_DIR_NAMES = ("aidlc-docs", "tools")
35
+ TOOLING_FILE_NAMES = ("trigger-agentic-dev-loop.sh",
36
+ "agentic-dev-loop/.engine-version")
35
37
 
36
38
 
37
39
  class HostBoundaryError(RuntimeError):
@@ -73,6 +75,7 @@ def tooling_rel_paths(cfg: "PipelineConfig", pipeline_path: str | Path,
73
75
  (base / cfg.agents_dir).resolve(),
74
76
  *((base / rel).resolve() for rel in cfg.additional_files),
75
77
  *(ws / name for name in TOOLING_DIR_NAMES),
78
+ *(ws / name for name in TOOLING_FILE_NAMES),
76
79
  ]
77
80
  rels = {p.relative_to(ws).as_posix() for p in candidates
78
81
  if p.is_relative_to(ws)}
@@ -0,0 +1,6 @@
1
+ """Consumer onboarding: init, doctor, recipes, and templates.
2
+
3
+ These modules are engine concerns (they render pipelines and probe the host);
4
+ the only core dependency is ``agentic_dev_loop.core.profiles``, the allowed
5
+ engineering direction.
6
+ """