agentic-dev-loop 1.0.0a30__tar.gz → 1.0.0a31__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of agentic-dev-loop might be problematic. Click here for more details.

Files changed (94) hide show
  1. {agentic_dev_loop-1.0.0a30/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a31}/PKG-INFO +23 -1
  2. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/README.md +22 -0
  3. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/pyproject.toml +2 -1
  4. agentic_dev_loop-1.0.0a31/src/agentic_dev_loop/engine/agent_error_guide.md +6 -0
  5. agentic_dev_loop-1.0.0a31/src/agentic_dev_loop/engine/agent_error_guide.py +26 -0
  6. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/stages.py +14 -12
  7. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31/src/agentic_dev_loop.egg-info}/PKG-INFO +23 -1
  8. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/SOURCES.txt +3 -0
  9. agentic_dev_loop-1.0.0a31/tests/test_agent_error_guide.py +184 -0
  10. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/LICENSE +0 -0
  11. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/setup.cfg +0 -0
  12. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/__init__.py +0 -0
  13. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/__init__.py +0 -0
  14. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/config.py +0 -0
  15. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/paths.py +0 -0
  16. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/results.py +0 -0
  17. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/core/verdict.py +0 -0
  18. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/__init__.py +0 -0
  19. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/actions.py +0 -0
  20. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/agents.py +0 -0
  21. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/cli.py +0 -0
  22. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/deps.py +0 -0
  23. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  24. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  25. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  26. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  27. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  28. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/graph.py +0 -0
  29. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  30. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/isolation.py +0 -0
  31. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/llm.py +0 -0
  32. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/loaders.py +0 -0
  33. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/mounts.py +0 -0
  34. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/outputs.py +0 -0
  35. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/presets.py +0 -0
  36. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/proc.py +0 -0
  37. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/registry.py +0 -0
  38. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  39. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  40. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/secrets.py +0 -0
  41. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  42. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  43. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/py.typed +0 -0
  44. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/viz/__init__.py +0 -0
  45. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/viz/__main__.py +0 -0
  46. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop/viz/server.py +0 -0
  47. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  48. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  49. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  50. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  51. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_028_declared_caches.py +0 -0
  52. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_029_merge_on_conflict.py +0 -0
  53. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_032_stop_when_green.py +0 -0
  54. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_033_brief_step.py +0 -0
  55. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_actions.py +0 -0
  56. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_agents.py +0 -0
  57. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_aidlc.py +0 -0
  58. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_cli.py +0 -0
  59. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_config.py +0 -0
  60. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_deps.py +0 -0
  61. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_dry_run.py +0 -0
  62. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_e2e_fixtures.py +0 -0
  63. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_egress_proxy.py +0 -0
  64. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_graph.py +0 -0
  65. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_hostboundary.py +0 -0
  66. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_hostboundary_integration.py +0 -0
  67. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_isolation.py +0 -0
  68. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_isolation_integration.py +0 -0
  69. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_isolation_pbt.py +0 -0
  70. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_llm.py +0 -0
  71. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_loaders.py +0 -0
  72. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_module_contract.py +0 -0
  73. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_outputs.py +0 -0
  74. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_outputs_pbt.py +0 -0
  75. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_proc.py +0 -0
  76. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_publish.py +0 -0
  77. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_qa_prompt.py +0 -0
  78. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_registry.py +0 -0
  79. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_repo_ascii.py +0 -0
  80. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_results.py +0 -0
  81. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_sdk_compat.py +0 -0
  82. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_secrets.py +0 -0
  83. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_secrets_integration.py +0 -0
  84. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_secrets_pipeline.py +0 -0
  85. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_source_resolution.py +0 -0
  86. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_stages.py +0 -0
  87. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_step_workspace.py +0 -0
  88. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_sync_back.py +0 -0
  89. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_trigger_script.py +0 -0
  90. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_unit5_config.py +0 -0
  91. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_unit5_pbt.py +0 -0
  92. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_unit5_pure.py +0 -0
  93. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/tests/test_verdict.py +0 -0
  94. {agentic_dev_loop-1.0.0a30 → agentic_dev_loop-1.0.0a31}/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.0a30
3
+ Version: 1.0.0a31
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -541,6 +541,28 @@ dotnet restore --locked-mode # installs exactly packages.lock.json
541
541
  Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
542
542
  to prove it holds from a clean cache before setting `deps_policy: locked`.
543
543
 
544
+ ## Agent-facing error guide
545
+
546
+ The engine bundles an always-on guide for the engine error codes an agent
547
+ can actually observe while running terminal commands. The guide lives at
548
+ `src/agentic_dev_loop/engine/agent_error_guide.md`, versioned with the engine
549
+ code that emits the codes, and is injected into every agent step's task
550
+ context at assembly time. There is no opt-in and no opt-out in v1; command
551
+ and action steps never receive it because no agent reads them.
552
+
553
+ Each agent-visible code has exactly one table row, shaped like this:
554
+
555
+ | Code | Meaning | Correct response |
556
+ | --- | --- | --- |
557
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
558
+
559
+ The guide is consumer-agnostic: it explains the general mechanism and never
560
+ names a consumer or a consumer domain rule ID. If an ID must appear inside a
561
+ quoted engine message, the placeholder `<domain-rule-id>` is used. Any engine
562
+ change that adds or changes an agent-visible error code must update the guide
563
+ in the same commit; the test suite enforces the pairing by parsing the
564
+ agent-visible `SandboxError` raise sites against the bundled table.
565
+
544
566
  ## Host boundary (security model)
545
567
 
546
568
  The orchestrator, tool layer, and the `commit` action run in the **host
@@ -499,6 +499,28 @@ dotnet restore --locked-mode # installs exactly packages.lock.json
499
499
  Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
500
500
  to prove it holds from a clean cache before setting `deps_policy: locked`.
501
501
 
502
+ ## Agent-facing error guide
503
+
504
+ The engine bundles an always-on guide for the engine error codes an agent
505
+ can actually observe while running terminal commands. The guide lives at
506
+ `src/agentic_dev_loop/engine/agent_error_guide.md`, versioned with the engine
507
+ code that emits the codes, and is injected into every agent step's task
508
+ context at assembly time. There is no opt-in and no opt-out in v1; command
509
+ and action steps never receive it because no agent reads them.
510
+
511
+ Each agent-visible code has exactly one table row, shaped like this:
512
+
513
+ | Code | Meaning | Correct response |
514
+ | --- | --- | --- |
515
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
516
+
517
+ The guide is consumer-agnostic: it explains the general mechanism and never
518
+ names a consumer or a consumer domain rule ID. If an ID must appear inside a
519
+ quoted engine message, the placeholder `<domain-rule-id>` is used. Any engine
520
+ change that adds or changes an agent-visible error code must update the guide
521
+ in the same commit; the test suite enforces the pairing by parsing the
522
+ agent-visible `SandboxError` raise sites against the bundled table.
523
+
502
524
  ## Host boundary (security model)
503
525
 
504
526
  The orchestrator, tool layer, and the `commit` action run in the **host
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a30"
7
+ version = "1.0.0a31"
8
8
  description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -68,6 +68,7 @@ where = ["src"]
68
68
 
69
69
  [tool.setuptools.package-data]
70
70
  agentic_dev_loop = ["py.typed"]
71
+ "agentic_dev_loop.engine" = ["*.md"]
71
72
  "agentic_dev_loop.engine.seccomp" = ["*.json"]
72
73
 
73
74
  [tool.pytest.ini_options]
@@ -0,0 +1,6 @@
1
+ | Code | Meaning | Correct response |
2
+ | --- | --- | --- |
3
+ | AE-SBX-008 | The sandbox refused a git mutation command. Only the single commit step may change git state; agents never commit, branch, tag, or otherwise mutate git. | Do not run the command or any git mutation again. Continue without touching git and leave committing to the commit step. |
4
+ | AE-SBX-010 | The sandbox could not create the execution container for the command, so the command did not run. | The command is not the problem: retry once; if it persists, mention the code in your final response and stop. |
5
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
6
+ | AE-SBX-018 | The network egress proxy for this step stopped, so the sandbox refused the command rather than run without enforcement. | The command is not the problem: do not retry it, mention the code in your final response, and stop. |
@@ -0,0 +1,26 @@
1
+ """Bundled agent-facing error guide (034).
2
+
3
+ The guide table is an engine-owned resource, versioned with the engine so the
4
+ codes and their guidance cannot drift. Every agent step's task context gets
5
+ the table injected at assembly time; command and action steps never do (no
6
+ agent reads them).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from importlib.resources import files
12
+
13
+ GUIDE_RESOURCE = "agent_error_guide.md"
14
+ GUIDE_HEADING = "Agent-facing error guide"
15
+
16
+
17
+ def load_guide() -> str:
18
+ """The bundled guide table text."""
19
+ return (files("agentic_dev_loop.engine")
20
+ .joinpath(GUIDE_RESOURCE)
21
+ .read_text(encoding="utf-8"))
22
+
23
+
24
+ def guide_section() -> str:
25
+ """The guide block injected into an agent step's task context."""
26
+ return f"## {GUIDE_HEADING}\n\n{load_guide()}"
@@ -32,6 +32,7 @@ from typing import TYPE_CHECKING
32
32
  from langgraph.graph import END
33
33
  from openhands.sdk import Conversation
34
34
 
35
+ from .agent_error_guide import guide_section
35
36
  from .agents import load_agent, register_sandbox_terminal
36
37
  from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
37
38
  from agentic_dev_loop.core.paths import _has_wildcards
@@ -364,12 +365,12 @@ def _artifact_file(runtime: Runtime, step_name: str) -> str | None:
364
365
  def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
365
366
  notes: list[str] | None = None,
366
367
  artifacts: dict[str, str] | None = None) -> str:
367
- """Assemble the attempt's shared message: subject, doc bundle, the
368
- canonical dependency-policy paragraph (021), the expected-output-paths
369
- section generated from step defs (a prompt merely naming files lets agents
370
- skip them), accumulated stage artifacts (027), and previous-attempt
371
- notes."""
372
- parts = [f"You are working on {subject}.\n"]
368
+ """Assemble the attempt's shared message: the always-on agent-facing error
369
+ guide (034), subject, doc bundle, the canonical dependency-policy paragraph
370
+ (021), the expected-output-paths section generated from step defs (a prompt
371
+ merely naming files lets agents skip them), accumulated stage artifacts
372
+ (027), and previous-attempt notes."""
373
+ parts = [f"You are working on {subject}.\n", guide_section()]
373
374
  for name, content in docs.items():
374
375
  parts.append(f"## {name}\n\n{content}")
375
376
  if artifacts:
@@ -541,13 +542,14 @@ def brief_inputs_hash(runtime: Runtime, step: StepConfig,
541
542
  def build_brief_context(runtime: Runtime, step: StepConfig,
542
543
  docs: dict[str, str],
543
544
  doc_sources: dict[str, str] | None = None) -> str:
544
- """Assemble the brief agent's task message (033): the resolved unit/plan
545
- docs (re-read from disk where sources are known), the closed
546
- ``brief_inputs.include`` file set, and the env-facts block. Prior verdicts
547
- and feedback are deliberately absent - the notes channel delivers them per
548
- attempt."""
545
+ """Assemble the brief agent's task message (033): the always-on
546
+ agent-facing error guide (034), the resolved unit/plan docs (re-read from
547
+ disk where sources are known), the closed ``brief_inputs.include`` file
548
+ set, and the env-facts block. Prior verdicts and feedback are deliberately
549
+ absent - the notes channel delivers them per attempt."""
549
550
  sections, env_facts = _brief_sections(runtime, step, docs, doc_sources)
550
- parts = [f"## {name}\n\n{content}" for name, content in sections.items()]
551
+ parts = [guide_section()]
552
+ parts += [f"## {name}\n\n{content}" for name, content in sections.items()]
551
553
  if env_facts:
552
554
  parts.append("## Environment facts\n\n"
553
555
  + "\n".join(f"- {fact}" for fact in env_facts))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a30
3
+ Version: 1.0.0a31
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -541,6 +541,28 @@ dotnet restore --locked-mode # installs exactly packages.lock.json
541
541
  Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
542
542
  to prove it holds from a clean cache before setting `deps_policy: locked`.
543
543
 
544
+ ## Agent-facing error guide
545
+
546
+ The engine bundles an always-on guide for the engine error codes an agent
547
+ can actually observe while running terminal commands. The guide lives at
548
+ `src/agentic_dev_loop/engine/agent_error_guide.md`, versioned with the engine
549
+ code that emits the codes, and is injected into every agent step's task
550
+ context at assembly time. There is no opt-in and no opt-out in v1; command
551
+ and action steps never receive it because no agent reads them.
552
+
553
+ Each agent-visible code has exactly one table row, shaped like this:
554
+
555
+ | Code | Meaning | Correct response |
556
+ | --- | --- | --- |
557
+ | AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
558
+
559
+ The guide is consumer-agnostic: it explains the general mechanism and never
560
+ names a consumer or a consumer domain rule ID. If an ID must appear inside a
561
+ quoted engine message, the placeholder `<domain-rule-id>` is used. Any engine
562
+ change that adds or changes an agent-visible error code must update the guide
563
+ in the same commit; the test suite enforces the pairing by parsing the
564
+ agent-visible `SandboxError` raise sites against the bundled table.
565
+
544
566
  ## Host boundary (security model)
545
567
 
546
568
  The orchestrator, tool layer, and the `commit` action run in the **host
@@ -16,6 +16,8 @@ src/agentic_dev_loop/core/results.py
16
16
  src/agentic_dev_loop/core/verdict.py
17
17
  src/agentic_dev_loop/engine/__init__.py
18
18
  src/agentic_dev_loop/engine/actions.py
19
+ src/agentic_dev_loop/engine/agent_error_guide.md
20
+ src/agentic_dev_loop/engine/agent_error_guide.py
19
21
  src/agentic_dev_loop/engine/agents.py
20
22
  src/agentic_dev_loop/engine/cli.py
21
23
  src/agentic_dev_loop/engine/deps.py
@@ -48,6 +50,7 @@ tests/test_029_merge_on_conflict.py
48
50
  tests/test_032_stop_when_green.py
49
51
  tests/test_033_brief_step.py
50
52
  tests/test_actions.py
53
+ tests/test_agent_error_guide.py
51
54
  tests/test_agents.py
52
55
  tests/test_aidlc.py
53
56
  tests/test_cli.py
@@ -0,0 +1,184 @@
1
+ """Agent-facing error guide (034).
2
+
3
+ The guide is an engine-owned bundled resource injected into every agent
4
+ step's task context. These tests pin the injection point, the one-line-per-code
5
+ table shape, the consumer-agnostic ASCII content, and the pairing between the
6
+ guide and the agent-visible ``SandboxError`` codes emitted by the engine.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ import re
13
+ from collections import Counter
14
+ from dataclasses import replace
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+ from openhands.sdk import LLM
19
+
20
+ from agentic_dev_loop.core.config import PipelineConfig
21
+ from agentic_dev_loop.engine.agent_error_guide import (
22
+ GUIDE_HEADING,
23
+ GUIDE_RESOURCE,
24
+ guide_section,
25
+ load_guide,
26
+ )
27
+ from agentic_dev_loop.engine.registry import default_registry
28
+ from agentic_dev_loop.engine.stages import (
29
+ Runtime,
30
+ build_brief_context,
31
+ build_context,
32
+ make_agent_node,
33
+ make_command_node,
34
+ )
35
+
36
+ REPO_ROOT = Path(__file__).resolve().parents[1]
37
+ ISOLATION = REPO_ROOT / "src" / "agentic_dev_loop" / "engine" / "isolation.py"
38
+
39
+ # The source-of-truth agent-visible codes. AE-SBX-012/013/017/019 are emitted
40
+ # from the same Sandbox.run surface but are internal: sticky infra failures
41
+ # (012/019) stop the run before the agent can respond, and 013/017 are
42
+ # engine-invariant/config errors that never describe a command the agent can
43
+ # correct.
44
+ AGENT_VISIBLE_CODES = {"AE-SBX-008", "AE-SBX-010", "AE-SBX-016", "AE-SBX-018"}
45
+ INTERNAL_SANDBOX_CODES = {"AE-SBX-012", "AE-SBX-013", "AE-SBX-017", "AE-SBX-019"}
46
+
47
+ _CODE_RE = re.compile(r"AE-SBX-\d{3}")
48
+
49
+
50
+ def make_cfg(steps, **over):
51
+ base = {"name": "t", "llm": {"model": "m", "api_key_env": "K"},
52
+ "agents_dir": "agents", "lockfiles": ["uv.lock"], "steps": steps}
53
+ base.update(over)
54
+ return PipelineConfig.model_validate(base)
55
+
56
+
57
+ def make_state(**over):
58
+ state = {"subject": "plan plan.md", "docs": {"plan.md": "THE PLAN"},
59
+ "artifacts": {}, "attempt": 1, "max_attempts": 3, "notes": [],
60
+ "failed": False, "outcome": None, "commit_allowed": False,
61
+ "step_verdicts": {}, "retry_target": None, "thread_id": "t1"}
62
+ state.update(over)
63
+ return state
64
+
65
+
66
+ @pytest.fixture
67
+ def runtime(tmp_path, fake_sandbox):
68
+ state_dir = tmp_path / ".pr"
69
+ state_dir.mkdir()
70
+ cfg = make_cfg([{"name": "dev", "agent": "dev",
71
+ "produces": ["implementation-summary.md"],
72
+ "on_pass": "success"}])
73
+ return Runtime(cfg=cfg, registry=default_registry(),
74
+ llm=LLM(model="openai/test-model", api_key="sk-test"),
75
+ agents_dir=tmp_path / "agents", state_dir=state_dir,
76
+ workspace=tmp_path, sandbox=fake_sandbox)
77
+
78
+
79
+ def _guide_codes(text: str) -> list[str]:
80
+ codes = []
81
+ for line in text.splitlines():
82
+ stripped = line.strip()
83
+ if not stripped.startswith("|"):
84
+ continue
85
+ cells = [c.strip() for c in stripped.strip("|").split("|")]
86
+ if cells and _CODE_RE.fullmatch(cells[0]):
87
+ codes.append(cells[0])
88
+ return codes
89
+
90
+
91
+ def _agent_path_error_codes() -> set[str]:
92
+ """Every AE-SBX code raised on the Sandbox.run surface an agent terminal
93
+ action can reach, parsed from the engine source."""
94
+ tree = ast.parse(ISOLATION.read_text(encoding="utf-8"), filename=str(ISOLATION))
95
+ target_nodes: list[ast.AST] = []
96
+ for node in tree.body:
97
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
98
+ if node.name == "_guard_git":
99
+ target_nodes.append(node)
100
+ elif isinstance(node, ast.ClassDef) and node.name == "DockerBackend":
101
+ for child in node.body:
102
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
103
+ if child.name in {"run", "_run_legacy", "_run_profile",
104
+ "_exec", "_start_profile", "_sync_back"}:
105
+ target_nodes.append(child)
106
+
107
+ found: set[str] = set()
108
+ for node in target_nodes:
109
+ for child in ast.walk(node):
110
+ if isinstance(child, ast.Raise) and isinstance(child.exc, ast.Call):
111
+ fn = child.exc.func
112
+ if isinstance(fn, ast.Name) and fn.id == "SandboxError":
113
+ for part in ast.walk(child.exc):
114
+ if isinstance(part, ast.Constant) and isinstance(part.value, str):
115
+ found.update(_CODE_RE.findall(part.value))
116
+ return found
117
+
118
+
119
+ class TestBundledGuide:
120
+ def test_loads_from_package_resource(self):
121
+ from importlib.resources import files
122
+
123
+ expected = (files("agentic_dev_loop.engine")
124
+ .joinpath(GUIDE_RESOURCE)
125
+ .read_text(encoding="utf-8"))
126
+ assert load_guide() == expected
127
+ assert "AE-SBX-016" in load_guide()
128
+
129
+ def test_section_wraps_table(self):
130
+ section = guide_section()
131
+ assert section.startswith(f"## {GUIDE_HEADING}\n\n")
132
+ assert "AE-SBX-016" in section
133
+
134
+ def test_ascii_and_consumer_agnostic(self):
135
+ text = load_guide()
136
+ assert text.isascii()
137
+ assert "consumer" not in text.lower()
138
+
139
+ def test_every_visible_code_has_exactly_one_line(self):
140
+ codes = _guide_codes(load_guide())
141
+ assert Counter(codes) == Counter(AGENT_VISIBLE_CODES)
142
+
143
+ def test_engine_agent_path_codes_pair_with_guide(self):
144
+ visible = _agent_path_error_codes() - INTERNAL_SANDBOX_CODES
145
+ assert visible == AGENT_VISIBLE_CODES
146
+
147
+
148
+ class TestInjection:
149
+ def test_agent_context_contains_guide_before_docs(self, runtime):
150
+ ctx = build_context(runtime, {"plan.md": "THE PLAN"}, "plan plan.md")
151
+ assert f"## {GUIDE_HEADING}" in ctx
152
+ assert "AE-SBX-016" in ctx
153
+ assert ctx.index(f"## {GUIDE_HEADING}") < ctx.index("## plan.md")
154
+
155
+ def test_brief_context_contains_guide_before_docs(self, runtime):
156
+ (runtime.workspace / "AGENTS.md").write_text("REPO RULES")
157
+ step = make_cfg([{
158
+ "name": "brief", "agent": "brief", "produces": "attempt-brief.md",
159
+ "brief_inputs": {"include": ["AGENTS.md"],
160
+ "env_facts": ["offline"]},
161
+ "on_pass": "success",
162
+ }]).steps[0]
163
+ ctx = build_brief_context(runtime, step, {"plan.md": "THE PLAN"})
164
+ assert f"## {GUIDE_HEADING}" in ctx
165
+ assert "AE-SBX-016" in ctx
166
+ assert ctx.index(f"## {GUIDE_HEADING}") < ctx.index("## plan.md")
167
+
168
+ def test_agent_node_message_contains_guide(self, runtime, fake_agents):
169
+ fake_agents.set("dev", [("respond", "done")])
170
+ make_agent_node(runtime, runtime.cfg.steps[0])(make_state())
171
+ (message,) = fake_agents.messages_for("dev")
172
+ assert f"## {GUIDE_HEADING}" in message
173
+ assert "AE-SBX-016" in message
174
+
175
+ def test_command_step_notes_do_not_gain_guide(self, runtime):
176
+ cfg = make_cfg([
177
+ {"name": "dev", "agent": "dev", "on_pass": "test"},
178
+ {"name": "test", "command": "false", "on_pass": "success",
179
+ "on_fail": {"goto": "dev", "retry": True}},
180
+ ])
181
+ rt = replace(runtime, cfg=cfg)
182
+ update = make_command_node(rt, cfg.steps[1])(make_state())
183
+ assert "notes" in update
184
+ assert all(f"## {GUIDE_HEADING}" not in note for note in update["notes"])