agentic-dev-loop 1.0.0a41__tar.gz → 1.0.0a43__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 (102) hide show
  1. {agentic_dev_loop-1.0.0a41/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a43}/PKG-INFO +3 -3
  2. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/README.md +0 -2
  3. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/pyproject.toml +9 -1
  4. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/core/__init__.py +1 -3
  5. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/core/config.py +0 -35
  6. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/stages.py +18 -93
  7. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43/src/agentic_dev_loop.egg-info}/PKG-INFO +3 -3
  8. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -1
  9. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop.egg-info/requires.txt +2 -0
  10. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_035_finalize_nudge.py +11 -4
  11. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_041_brief_coverage.py +28 -0
  12. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_agents.py +31 -0
  13. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_config.py +24 -0
  14. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_sdk_compat.py +7 -7
  15. agentic_dev_loop-1.0.0a41/tests/test_032_stop_when_green.py +0 -363
  16. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/LICENSE +0 -0
  17. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/setup.cfg +0 -0
  18. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/__init__.py +0 -0
  19. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/core/paths.py +0 -0
  20. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/core/results.py +0 -0
  21. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/core/verdict.py +0 -0
  22. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/__init__.py +0 -0
  23. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/actions.py +0 -0
  24. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  25. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  26. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/agents.py +0 -0
  27. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  28. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  29. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/cli.py +0 -0
  30. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/containers.py +0 -0
  31. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/deps.py +0 -0
  32. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  33. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  34. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  35. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  36. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  37. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  38. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/graph.py +0 -0
  39. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  40. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/isolation.py +0 -0
  41. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/llm.py +0 -0
  42. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/loaders.py +0 -0
  43. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/mounts.py +0 -0
  44. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/outputs.py +0 -0
  45. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/presets.py +0 -0
  46. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/proc.py +0 -0
  47. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/registry.py +0 -0
  48. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  49. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  50. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/secrets.py +0 -0
  51. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  52. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  53. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/py.typed +0 -0
  54. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/viz/__init__.py +0 -0
  55. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/viz/__main__.py +0 -0
  56. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop/viz/server.py +0 -0
  57. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  58. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  59. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  60. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_028_declared_caches.py +0 -0
  61. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_029_merge_on_conflict.py +0 -0
  62. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_033_brief_step.py +0 -0
  63. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  64. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_actions.py +0 -0
  65. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_agent_error_guide.py +0 -0
  66. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_aidlc.py +0 -0
  67. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_cli.py +0 -0
  68. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_containers.py +0 -0
  69. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_deps.py +0 -0
  70. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_dry_run.py +0 -0
  71. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_e2e_fixtures.py +0 -0
  72. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_egress_proxy.py +0 -0
  73. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_graph.py +0 -0
  74. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_hostboundary.py +0 -0
  75. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_hostboundary_integration.py +0 -0
  76. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_isolation.py +0 -0
  77. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_isolation_integration.py +0 -0
  78. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_isolation_pbt.py +0 -0
  79. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_llm.py +0 -0
  80. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_loaders.py +0 -0
  81. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_module_contract.py +0 -0
  82. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_outputs.py +0 -0
  83. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_outputs_pbt.py +0 -0
  84. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_proc.py +0 -0
  85. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_publish.py +0 -0
  86. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_qa_prompt.py +0 -0
  87. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_registry.py +0 -0
  88. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_repo_ascii.py +0 -0
  89. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_results.py +0 -0
  90. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_secrets.py +0 -0
  91. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_secrets_integration.py +0 -0
  92. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_secrets_pipeline.py +0 -0
  93. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_source_resolution.py +0 -0
  94. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_stages.py +0 -0
  95. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_step_workspace.py +0 -0
  96. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_sync_back.py +0 -0
  97. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_trigger_script.py +0 -0
  98. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_unit5_config.py +0 -0
  99. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_unit5_pbt.py +0 -0
  100. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_unit5_pure.py +0 -0
  101. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/tests/test_verdict.py +0 -0
  102. {agentic_dev_loop-1.0.0a41 → agentic_dev_loop-1.0.0a43}/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.0a41
3
+ Version: 1.0.0a43
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
@@ -33,6 +33,8 @@ Requires-Dist: agent-client-protocol<1.0.0
33
33
  Requires-Dist: langgraph>=1.2
34
34
  Requires-Dist: langgraph-checkpoint-sqlite>=3.0
35
35
  Requires-Dist: filelock>=4.2.1
36
+ Requires-Dist: pyasn1>=0.6.5
37
+ Requires-Dist: pypdf>=6.21.4
36
38
  Provides-Extra: dev
37
39
  Requires-Dist: hypothesis>=6.0; extra == "dev"
38
40
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -213,7 +215,6 @@ steps:
213
215
  agent: dev
214
216
  produces: implementation-summary.md
215
217
  terminal_timeout: 900
216
- stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
217
218
  on_pass: test
218
219
 
219
220
  - name: test
@@ -249,7 +250,6 @@ steps:
249
250
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
250
251
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
251
252
  | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
252
- | `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
253
253
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
254
254
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
255
255
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
@@ -171,7 +171,6 @@ steps:
171
171
  agent: dev
172
172
  produces: implementation-summary.md
173
173
  terminal_timeout: 900
174
- stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
175
174
  on_pass: test
176
175
 
177
176
  - name: test
@@ -207,7 +206,6 @@ steps:
207
206
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
208
207
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
209
208
  | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
210
- | `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
211
209
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
212
210
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
213
211
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a41"
7
+ version = "1.0.0a43"
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"
@@ -47,6 +47,14 @@ dependencies = [
47
47
  # 3.32.7 carries CVE-2026-112907/-112913/-112940 (fixed in 4.2.1); pin a
48
48
  # floor so the restore step's pip-audit stays green.
49
49
  "filelock>=4.2.1",
50
+ # pyasn1 is a transitive dep (openhands-sdk, rsa, google-auth). 0.6.4
51
+ # carries CVE-2026-119194/-119195/-119196/-119197 (fixed in 0.6.5); pin a
52
+ # floor so the restore step's pip-audit stays green.
53
+ "pyasn1>=0.6.5",
54
+ # pypdf is a transitive dep (openhands-tools document loaders). 6.21.2
55
+ # carries CVE-2026-119203/-119204/-119205/-119206/-119207/-119208/-119209
56
+ # (fixed in 6.21.4); pin a floor so the restore step's pip-audit stays green.
57
+ "pypdf>=6.21.4",
50
58
  ]
51
59
 
52
60
  [project.urls]
@@ -6,8 +6,7 @@ test fails if this package imports engine/viz or heavy deps.
6
6
 
7
7
  from .config import (PipelineConfig, StepConfig, LlmConfig, IsolationConfig,
8
8
  VerdictSpec, Route, route_target, load_config,
9
- default_state_dir, TERMINALS, MountSpec, EgressSpec,
10
- StopWhenSpec)
9
+ default_state_dir, TERMINALS, MountSpec, EgressSpec)
11
10
  from .results import Result
12
11
  from .verdict import Verdict, VerdictError, parse_verdict, RETRY
13
12
 
@@ -15,6 +14,5 @@ __all__ = [
15
14
  "PipelineConfig", "StepConfig", "LlmConfig", "IsolationConfig",
16
15
  "VerdictSpec", "Route", "route_target", "load_config",
17
16
  "default_state_dir", "TERMINALS", "MountSpec", "EgressSpec",
18
- "StopWhenSpec",
19
17
  "Result", "Verdict", "VerdictError", "parse_verdict", "RETRY",
20
18
  ]
@@ -306,36 +306,6 @@ class EgressSpec(BaseModel):
306
306
  return [_validate_egress_target(t) for t in v]
307
307
 
308
308
 
309
- class StopWhenSpec(BaseModel):
310
- """Optional ``stop_when:`` termination signal on an agent step (032).
311
-
312
- ``green_command`` is a regex matched against the agent's terminal
313
- observation command; a matching observation that exits 0 - and no
314
- workspace writes since - triggers one nudge turn, then
315
- ``post_green_turns`` further turns before the engine closes the
316
- conversation. Absent ``stop_when`` keeps the current run-to-finish
317
- behaviour unchanged.
318
- """
319
-
320
- model_config = ConfigDict(extra="forbid")
321
-
322
- green_command: str
323
- post_green_turns: int = Field(default=2, ge=0)
324
-
325
- @field_validator("green_command")
326
- @classmethod
327
- def _valid_regex(cls, v: str) -> str:
328
- if not isinstance(v, str) or not v.strip():
329
- raise ValueError(
330
- "stop_when.green_command must be a non-empty string")
331
- try:
332
- re.compile(v)
333
- except re.error as e:
334
- raise ValueError(
335
- f"stop_when.green_command is not a valid regex: {e}") from e
336
- return v
337
-
338
-
339
309
  class BriefInputsSpec(BaseModel):
340
310
  """Optional ``brief_inputs:`` on an agent step (033): the closed input
341
311
  list a brief agent digests once per run. ``include`` is repo-relative
@@ -387,7 +357,6 @@ class StepConfig(BaseModel):
387
357
  max_iterations: int | None = None
388
358
  timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
389
359
  terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
390
- stop_when: StopWhenSpec | None = None # agent steps only (032)
391
360
  finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
392
361
  brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
393
362
  custom_prompt: str | None = None # agent steps only (040); file path resolved at load
@@ -488,10 +457,6 @@ class StepConfig(BaseModel):
488
457
  raise ValueError(
489
458
  f"step {self.name!r}: 'terminal_timeout' requires 'agent'"
490
459
  )
491
- if self.stop_when is not None and self.agent is None:
492
- raise ValueError(
493
- f"step {self.name!r}: 'stop_when' requires 'agent'"
494
- )
495
460
  if self.finalize_nudge_turns is not None and self.agent is None:
496
461
  raise ValueError(
497
462
  f"step {self.name!r}: 'finalize_nudge_turns' requires 'agent'"
@@ -21,7 +21,6 @@ from __future__ import annotations
21
21
 
22
22
  import hashlib
23
23
  import os
24
- import re
25
24
  import subprocess
26
25
  import sys
27
26
  import traceback
@@ -70,9 +69,9 @@ def custom_prompt_section(text: str) -> str:
70
69
 
71
70
 
72
71
  class _TurnPauseController:
73
- """Turn-level pause callback (032): pause the SDK run loop after every
72
+ """Turn-level pause callback: pause the SDK run loop after every
74
73
  agent-sourced event so the caller can drive one turn at a time. The
75
- finalize nudge (035) reuses this hook without any stop-when state."""
74
+ finalize nudge (035) reuses this hook."""
76
75
 
77
76
  def __init__(self) -> None:
78
77
  self.conversation = None
@@ -108,65 +107,6 @@ class _FinalizeNudgeController(_TurnPauseController):
108
107
  return self._TEMPLATE.format(n=self.turns)
109
108
 
110
109
 
111
- class _StopWhenController(_TurnPauseController):
112
- """Turn-level stop-when-green observation (032).
113
-
114
- Pure per-event state machine - no knowledge of how the conversation is
115
- driven. ``on_event`` is a SDK conversation callback: it records the most
116
- recent matching terminal observation and any subsequent file-editor
117
- write, and (via the base pause) pauses the conversation after every
118
- agent-sourced event so the caller can drive one turn at a time.
119
- """
120
-
121
- NUDGE = "Step complete: produce your final response now"
122
- _WRITE_COMMANDS = frozenset({"create", "str_replace", "insert", "undo_edit"})
123
-
124
- def __init__(self, spec) -> None:
125
- super().__init__()
126
- self._pattern = re.compile(spec.green_command)
127
- self.post_green_turns = spec.post_green_turns
128
- self.green_hit = False
129
- self.writes_since_green = False
130
- self.nudged = False
131
- self.post_nudge_turns = 0
132
-
133
- @property
134
- def green_signal(self) -> bool:
135
- return self.green_hit and not self.writes_since_green
136
-
137
- def on_event(self, event) -> None:
138
- super().on_event(event)
139
- if self.conversation is None:
140
- return
141
- from openhands.sdk.event import ObservationEvent
142
-
143
- if isinstance(event, ObservationEvent):
144
- self._record_observation(event)
145
-
146
- def on_terminal_result(self, command: str | None, exit_code: int | None,
147
- wrote_workspace: bool) -> None:
148
- """Feed the sandbox terminal result channel (032): a matching green
149
- command is the signal; any other terminal write resets it."""
150
- if command is not None and self._pattern.search(command):
151
- self.green_hit = exit_code == 0
152
- self.writes_since_green = False
153
- elif wrote_workspace:
154
- self.writes_since_green = True
155
-
156
- def _record_observation(self, event) -> None:
157
- observation = event.observation
158
- tool_name = event.tool_name
159
- if tool_name == "terminal":
160
- command = getattr(observation, "command", None)
161
- if command is not None and self._pattern.search(command):
162
- self.green_hit = getattr(observation, "exit_code", None) == 0
163
- self.writes_since_green = False
164
- elif tool_name == "file_editor":
165
- command = getattr(observation, "command", None)
166
- if command in self._WRITE_COMMANDS and not observation.is_error:
167
- self.writes_since_green = True
168
-
169
-
170
110
  @dataclass
171
111
  class Runtime:
172
112
  """Everything stage nodes need that isn't in LoopState: pipeline config,
@@ -201,9 +141,6 @@ class Runtime:
201
141
  try:
202
142
  from openhands.sdk.conversation.state import ConversationExecutionStatus
203
143
 
204
- controller = None
205
- if step is not None and step.stop_when is not None:
206
- controller = _StopWhenController(step.stop_when)
207
144
  finalize_controller = None
208
145
  if step is not None and step.finalize_nudge_turns is not None:
209
146
  finalize_controller = _FinalizeNudgeController(
@@ -232,29 +169,23 @@ class Runtime:
232
169
  egress_policy=egress_policy,
233
170
  terminal_timeout=(step.terminal_timeout
234
171
  if step is not None else None),
235
- on_terminal_result=(controller.on_terminal_result
236
- if controller is not None else None),
237
172
  secrets=(step_secret_env(self.cfg, step, self.secrets)
238
173
  if step is not None else None),
239
174
  forbidden_commands=forbidden_commands,
240
175
  allowlist=allowlist)
241
176
  agent = load_agent(self.agents_dir, agent_name, self.llm)
242
177
  callbacks = []
243
- if controller is not None:
244
- callbacks.append(controller.on_event)
245
178
  if finalize_controller is not None:
246
179
  callbacks.append(finalize_controller.on_event)
247
180
  conv = Conversation(agent=agent, workspace=str(self.workspace),
248
181
  max_iteration_per_run=max_iterations,
249
182
  callbacks=callbacks or None)
250
- if controller is not None:
251
- controller.attach(conv)
252
183
  if finalize_controller is not None:
253
184
  finalize_controller.attach(conv)
254
185
  conv.send_message(message)
255
- if controller is not None or finalize_controller is not None:
256
- self._run_stop_when_loop(conv, controller, max_iterations,
257
- finalize_controller)
186
+ if finalize_controller is not None:
187
+ self._run_finalize_loop(conv, max_iterations,
188
+ finalize_controller)
258
189
  else:
259
190
  conv.run()
260
191
 
@@ -295,10 +226,9 @@ class Runtime:
295
226
  if conv is not None:
296
227
  conv.close()
297
228
 
298
- def _run_stop_when_loop(self, conv,
299
- controller: _StopWhenController | None,
300
- max_iterations: int,
301
- finalize_controller: _FinalizeNudgeController | None = None) -> None:
229
+ def _run_finalize_loop(self, conv,
230
+ max_iterations: int,
231
+ finalize_controller: _FinalizeNudgeController | None = None) -> None:
302
232
  """Drive one turn at a time so turn-level hooks can inject their
303
233
  nudges and enforce budgets. A controller's callback pauses the SDK
304
234
  run loop after every agent-sourced event, so each ``conv.run()`` here
@@ -327,19 +257,6 @@ class Runtime:
327
257
  finalize_controller.nudged = True
328
258
  continue
329
259
 
330
- if controller is not None:
331
- if not controller.nudged and controller.green_signal:
332
- conv.send_message(controller.NUDGE)
333
- controller.nudged = True
334
- if controller.post_green_turns == 0:
335
- break
336
- continue
337
-
338
- if controller.nudged:
339
- controller.post_nudge_turns += 1
340
- if controller.post_nudge_turns >= controller.post_green_turns:
341
- break
342
-
343
260
 
344
261
  def prompt_dir(runtime: Runtime) -> str:
345
262
  """state_dir as agents must address it. Agents resolve prompt paths
@@ -661,8 +578,16 @@ def build_brief_context(runtime: Runtime, step: StepConfig,
661
578
  parts.append(
662
579
  "## Coverage retry\n\n"
663
580
  "The captured brief is missing the following required items. "
664
- "Regenerate the digest so every listed item appears on its own "
665
- "line, verbatim where the contract requires verbatim:\n\n"
581
+ "Regenerate the digest so every listed item occupies its own "
582
+ "line, exactly as listed: the item alone on the line, no "
583
+ "trailing colon and no content. Put the compressed content on "
584
+ "the following line:\n\n"
585
+ "- general_principles\n"
586
+ " Simplest thing that works; defer decisions to the last "
587
+ "responsible moment.\n\n"
588
+ "A line like `general_principles: Simplest thing that works; "
589
+ "...` does not satisfy coverage; the item must be the only "
590
+ "text on its line:\n\n"
666
591
  + coverage_note)
667
592
  return "\n\n".join(parts)
668
593
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a41
3
+ Version: 1.0.0a43
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
@@ -33,6 +33,8 @@ Requires-Dist: agent-client-protocol<1.0.0
33
33
  Requires-Dist: langgraph>=1.2
34
34
  Requires-Dist: langgraph-checkpoint-sqlite>=3.0
35
35
  Requires-Dist: filelock>=4.2.1
36
+ Requires-Dist: pyasn1>=0.6.5
37
+ Requires-Dist: pypdf>=6.21.4
36
38
  Provides-Extra: dev
37
39
  Requires-Dist: hypothesis>=6.0; extra == "dev"
38
40
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -213,7 +215,6 @@ steps:
213
215
  agent: dev
214
216
  produces: implementation-summary.md
215
217
  terminal_timeout: 900
216
- stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
217
218
  on_pass: test
218
219
 
219
220
  - name: test
@@ -249,7 +250,6 @@ steps:
249
250
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
250
251
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
251
252
  | `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
252
- | `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
253
253
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
254
254
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
255
255
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
@@ -51,7 +51,6 @@ src/agentic_dev_loop/viz/__main__.py
51
51
  src/agentic_dev_loop/viz/server.py
52
52
  tests/test_028_declared_caches.py
53
53
  tests/test_029_merge_on_conflict.py
54
- tests/test_032_stop_when_green.py
55
54
  tests/test_033_brief_step.py
56
55
  tests/test_035_finalize_nudge.py
57
56
  tests/test_036_banned_commands_and_lane_boundary.py
@@ -6,6 +6,8 @@ agent-client-protocol<1.0.0
6
6
  langgraph>=1.2
7
7
  langgraph-checkpoint-sqlite>=3.0
8
8
  filelock>=4.2.1
9
+ pyasn1>=0.6.5
10
+ pypdf>=6.21.4
9
11
 
10
12
  [dev]
11
13
  hypothesis>=6.0
@@ -1,5 +1,5 @@
1
1
  """Finalize nudge (035): config surface, the turn-index injection, and the
2
- exactly-once guard on the shared 032 turn loop."""
2
+ exactly-once guard on the shared turn loop."""
3
3
 
4
4
  from pathlib import Path
5
5
  from types import SimpleNamespace
@@ -90,6 +90,13 @@ class TestFinalizeNudgeConfig:
90
90
  with pytest.raises(ValidationError, match="requires 'agent'"):
91
91
  _load(tmp_path, data)
92
92
 
93
+ def test_stop_when_key_rejected(self, tmp_path):
94
+ data = _base()
95
+ data["steps"] = [{"name": "dev", "agent": "dev",
96
+ "stop_when": {"green_command": "pytest"}}]
97
+ with pytest.raises(ValidationError, match="stop_when"):
98
+ _load(tmp_path, data)
99
+
93
100
 
94
101
  class TestFinalizeNudgeLoop:
95
102
  def _conv(self, finish_after):
@@ -114,7 +121,7 @@ class TestFinalizeNudgeLoop:
114
121
  def test_nudge_at_configured_turn_and_only_once(self):
115
122
  conv = self._conv(finish_after=9)
116
123
  controller = _FinalizeNudgeController(3)
117
- Runtime._run_stop_when_loop(None, conv, None, max_iterations=10,
124
+ Runtime._run_finalize_loop(None, conv, max_iterations=10,
118
125
  finalize_controller=controller)
119
126
  assert controller.nudged is True
120
127
  assert conv.sent == [(7, NUDGE)]
@@ -122,7 +129,7 @@ class TestFinalizeNudgeLoop:
122
129
  def test_no_nudge_when_finished_before_threshold(self):
123
130
  conv = self._conv(finish_after=3)
124
131
  controller = _FinalizeNudgeController(3)
125
- Runtime._run_stop_when_loop(None, conv, None, max_iterations=10,
132
+ Runtime._run_finalize_loop(None, conv, max_iterations=10,
126
133
  finalize_controller=controller)
127
134
  assert controller.nudged is False
128
135
  assert conv.sent == []
@@ -130,7 +137,7 @@ class TestFinalizeNudgeLoop:
130
137
  def test_turn_cap_still_applies_when_nudge_ignored(self):
131
138
  conv = self._conv(finish_after=999)
132
139
  controller = _FinalizeNudgeController(3)
133
- Runtime._run_stop_when_loop(None, conv, None, max_iterations=5,
140
+ Runtime._run_finalize_loop(None, conv, max_iterations=5,
134
141
  finalize_controller=controller)
135
142
  assert controller.nudged is True
136
143
  assert conv.state.execution_status == ConversationExecutionStatus.ERROR
@@ -17,6 +17,7 @@ from agentic_dev_loop.engine.registry import default_registry
17
17
  from agentic_dev_loop.engine.stages import (
18
18
  Runtime,
19
19
  brief_inputs_hash,
20
+ build_brief_context,
20
21
  make_brief_node,
21
22
  )
22
23
 
@@ -176,6 +177,33 @@ class TestMissingAnchors:
176
177
  ["offline; never fetch"]) == []
177
178
 
178
179
 
180
+ class TestCoverageNoteWording:
181
+ """The retry note must state the line rule unambiguously (043): a
182
+ correction that the model can misread as "the item starts its own line"
183
+ produced anchor: content one-liners for three straight retries in run
184
+ 5367f0c488d8."""
185
+
186
+ def test_note_states_line_rule_and_example(self, runtime):
187
+ note = "- mock policy"
188
+ message = build_brief_context(
189
+ runtime, runtime.cfg.steps[0], {"plan.md": "THE PLAN"},
190
+ coverage_note=note)
191
+ assert "## Coverage retry" in message
192
+ assert ("the item alone on the line, no trailing colon and no "
193
+ "content") in message
194
+ assert "- general_principles\n Simplest thing that works" in message
195
+ assert "does not satisfy coverage" in message
196
+ assert "the item must be the only text on its line" in message
197
+ assert note in message
198
+
199
+ def test_note_keeps_missing_items_verbatim(self, runtime):
200
+ note = "- mock policy\n- default development mode \u2014 ask before coding"
201
+ message = build_brief_context(
202
+ runtime, runtime.cfg.steps[0], {"plan.md": "THE PLAN"},
203
+ coverage_note=note)
204
+ assert "\n\n" + note in message
205
+
206
+
179
207
  class TestBriefNodeCoverage:
180
208
  def test_miss_returns_retry_note_and_reruns(self, runtime, fake_agents):
181
209
  good = _brief_ok()
@@ -188,6 +188,37 @@ class TestSandboxTerminalExecutor:
188
188
  assert fake_sandbox.requests[0].network is False
189
189
 
190
190
 
191
+ class TestTerminalTimeoutOverride:
192
+ def test_defaults_to_terminal_timeout(self, fake_sandbox, tmp_path):
193
+ from openhands.tools.terminal import TerminalAction
194
+
195
+ from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
196
+ ex = SandboxTerminalExecutor(
197
+ fake_sandbox, workspace=str(tmp_path), env={"PATH": "/bin"},
198
+ terminal_timeout=900)
199
+ ex(TerminalAction(command="echo hi"), None)
200
+ assert fake_sandbox.requests[0].timeout == 900
201
+
202
+ def test_action_timeout_wins(self, fake_sandbox, tmp_path):
203
+ from openhands.tools.terminal import TerminalAction
204
+
205
+ from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
206
+ ex = SandboxTerminalExecutor(
207
+ fake_sandbox, workspace=str(tmp_path), env={"PATH": "/bin"},
208
+ terminal_timeout=900)
209
+ ex(TerminalAction(command="echo hi", timeout=30), None)
210
+ assert fake_sandbox.requests[0].timeout == 30
211
+
212
+ def test_unset_keeps_none(self, fake_sandbox, tmp_path):
213
+ from openhands.tools.terminal import TerminalAction
214
+
215
+ from agentic_dev_loop.engine.agents import SandboxTerminalExecutor
216
+ ex = SandboxTerminalExecutor(
217
+ fake_sandbox, workspace=str(tmp_path), env={"PATH": "/bin"})
218
+ ex(TerminalAction(command="echo hi"), None)
219
+ assert fake_sandbox.requests[0].timeout is None
220
+
221
+
191
222
  class TestRegisterSandboxTerminal:
192
223
  def test_resolves_to_sandbox_executor(self, fake_sandbox, tmp_path):
193
224
  from openhands.sdk.tool.registry import resolve_tool
@@ -331,6 +331,30 @@ def test_timeout_without_command_rejected(tmp_path):
331
331
  _load(tmp_path, data)
332
332
 
333
333
 
334
+ def test_terminal_timeout_round_trip(tmp_path):
335
+ data = _base()
336
+ data["steps"][0]["terminal_timeout"] = 900
337
+ assert _load(tmp_path, data).steps[0].terminal_timeout == 900
338
+
339
+
340
+ @pytest.mark.parametrize("step", [
341
+ {"name": "x", "command": "true", "terminal_timeout": 900},
342
+ {"name": "x", "action": "commit", "terminal_timeout": 900},
343
+ ])
344
+ def test_terminal_timeout_requires_agent(tmp_path, step):
345
+ data = _base()
346
+ data["steps"] = [step]
347
+ with pytest.raises(ValidationError, match="'terminal_timeout' requires 'agent'"):
348
+ _load(tmp_path, data)
349
+
350
+
351
+ def test_negative_terminal_timeout_rejected(tmp_path):
352
+ data = _base()
353
+ data["steps"][0]["terminal_timeout"] = -1
354
+ with pytest.raises(ValidationError):
355
+ _load(tmp_path, data)
356
+
357
+
334
358
  class TestCustomPrompt:
335
359
  def test_defaults_none(self, tmp_path):
336
360
  assert _load(tmp_path, _base()).steps[0].custom_prompt is None
@@ -91,29 +91,29 @@ def test_canary_sdk_terminal_seam_still_present():
91
91
 
92
92
 
93
93
  def test_canary_sdk_turn_level_pause_seam_still_present():
94
- """Canary for the 032 turn-level stop-when-green seam (COMPAT-SHIMS.md):
94
+ """Canary for the 035 finalize-nudge turn loop seam (COMPAT-SHIMS.md):
95
95
  passes while the SDK still accepts ``callbacks`` on LocalConversation,
96
96
  passes agent-sourced events to them via ``agent.step(on_event=...)``, and
97
97
  breaks its run loop on PAUSED; fails once any of those disappear - the
98
- _StopWhenController pause loop is then dead and must be replaced with the
98
+ _TurnPauseController pause loop is then dead and must be replaced with the
99
99
  SDK's supported turn-level pause mechanism."""
100
100
  import inspect
101
101
 
102
102
  from openhands.sdk.conversation.impl.local_conversation import LocalConversation
103
103
 
104
104
  assert "callbacks" in inspect.signature(LocalConversation.__init__).parameters, (
105
- "The SDK's LocalConversation no longer accepts `callbacks=` - the 032 "
106
- "turn-level stop-when-green seam in src/agentic_dev_loop/engine/stages.py "
105
+ "The SDK's LocalConversation no longer accepts `callbacks=` - the 035 "
106
+ "finalize-nudge turn loop seam in src/agentic_dev_loop/engine/stages.py "
107
107
  "is dead. Adopt the SDK's supported pause mechanism and update "
108
108
  "COMPAT-SHIMS.md.")
109
109
  run_src = inspect.getsource(LocalConversation.run)
110
110
  assert "on_event=self._on_event" in run_src, (
111
111
  "The SDK's LocalConversation.run no longer passes its composed callback "
112
- "to agent.step - the 032 callback seam no longer receives agent-sourced "
112
+ "to agent.step - the 035 callback seam no longer receives agent-sourced "
113
113
  "events. Update COMPAT-SHIMS.md.")
114
114
  assert "ConversationExecutionStatus.PAUSED" in run_src, (
115
- "The SDK's LocalConversation.run no longer breaks on PAUSED - the 032 "
116
- "one-turn run loop in src/agentic_dev_loop/engine/stages.py is dead. "
115
+ "The SDK's LocalConversation.run no longer breaks on PAUSED - the 035 "
116
+ "finalize-nudge turn loop in src/agentic_dev_loop/engine/stages.py is dead. "
117
117
  "Adopt the SDK's supported pause mechanism and update COMPAT-SHIMS.md.")
118
118
 
119
119