agentic-dev-loop 1.0.0a34__tar.gz → 1.0.0a35__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 (98) hide show
  1. {agentic_dev_loop-1.0.0a34/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a35}/PKG-INFO +1 -1
  2. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/pyproject.toml +1 -1
  3. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/isolation.py +1 -0
  4. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/sync_back.py +14 -3
  5. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35/src/agentic_dev_loop.egg-info}/PKG-INFO +1 -1
  6. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_isolation.py +32 -0
  7. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_sync_back.py +46 -0
  8. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/LICENSE +0 -0
  9. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/README.md +0 -0
  10. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/setup.cfg +0 -0
  11. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/__init__.py +0 -0
  12. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/core/__init__.py +0 -0
  13. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/core/config.py +0 -0
  14. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/core/paths.py +0 -0
  15. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/core/results.py +0 -0
  16. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/core/verdict.py +0 -0
  17. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/__init__.py +0 -0
  18. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/actions.py +0 -0
  19. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  20. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  21. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/agents.py +0 -0
  22. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  23. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/cli.py +0 -0
  24. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/deps.py +0 -0
  25. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  26. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  27. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  28. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  29. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  30. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  31. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/graph.py +0 -0
  32. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  33. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/llm.py +0 -0
  34. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/loaders.py +0 -0
  35. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/mounts.py +0 -0
  36. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/outputs.py +0 -0
  37. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/presets.py +0 -0
  38. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/proc.py +0 -0
  39. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/registry.py +0 -0
  40. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  41. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  42. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/secrets.py +0 -0
  43. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/stages.py +0 -0
  44. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  45. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/py.typed +0 -0
  46. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/viz/__init__.py +0 -0
  47. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/viz/__main__.py +0 -0
  48. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop/viz/server.py +0 -0
  49. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  50. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  51. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  52. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  53. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  54. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_028_declared_caches.py +0 -0
  55. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_029_merge_on_conflict.py +0 -0
  56. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_032_stop_when_green.py +0 -0
  57. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_033_brief_step.py +0 -0
  58. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_035_finalize_nudge.py +0 -0
  59. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  60. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_actions.py +0 -0
  61. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_agent_error_guide.py +0 -0
  62. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_agents.py +0 -0
  63. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_aidlc.py +0 -0
  64. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_cli.py +0 -0
  65. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_config.py +0 -0
  66. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_deps.py +0 -0
  67. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_dry_run.py +0 -0
  68. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_e2e_fixtures.py +0 -0
  69. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_egress_proxy.py +0 -0
  70. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_graph.py +0 -0
  71. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_hostboundary.py +0 -0
  72. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_hostboundary_integration.py +0 -0
  73. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_isolation_integration.py +0 -0
  74. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_isolation_pbt.py +0 -0
  75. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_llm.py +0 -0
  76. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_loaders.py +0 -0
  77. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_module_contract.py +0 -0
  78. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_outputs.py +0 -0
  79. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_outputs_pbt.py +0 -0
  80. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_proc.py +0 -0
  81. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_publish.py +0 -0
  82. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_qa_prompt.py +0 -0
  83. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_registry.py +0 -0
  84. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_repo_ascii.py +0 -0
  85. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_results.py +0 -0
  86. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_sdk_compat.py +0 -0
  87. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_secrets.py +0 -0
  88. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_secrets_integration.py +0 -0
  89. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_secrets_pipeline.py +0 -0
  90. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_source_resolution.py +0 -0
  91. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_stages.py +0 -0
  92. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_step_workspace.py +0 -0
  93. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_trigger_script.py +0 -0
  94. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_unit5_config.py +0 -0
  95. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_unit5_pbt.py +0 -0
  96. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_unit5_pure.py +0 -0
  97. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/tests/test_verdict.py +0 -0
  98. {agentic_dev_loop-1.0.0a34 → agentic_dev_loop-1.0.0a35}/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.0a34
3
+ Version: 1.0.0a35
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a34"
7
+ version = "1.0.0a35"
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"
@@ -1250,6 +1250,7 @@ class DockerBackend(Sandbox):
1250
1250
  "event": "sync_back",
1251
1251
  "synced_paths": list(report.synced_paths),
1252
1252
  "merged_paths": list(report.merged_paths),
1253
+ "host_added_paths": list(report.host_added_paths),
1253
1254
  })
1254
1255
  return report
1255
1256
 
@@ -46,6 +46,7 @@ class SyncBackReport:
46
46
  conflicts: tuple[str, ...]
47
47
  merged_paths: tuple[str, ...] = ()
48
48
  lane_violations: tuple[str, ...] = ()
49
+ host_added_paths: tuple[str, ...] = ()
49
50
  step_id: str | None = None
50
51
  timestamp: str = ""
51
52
 
@@ -217,7 +218,13 @@ def sync_back(export_root: str | Path, real_repo: str | Path,
217
218
  real = real_files.get(rel)
218
219
  base = baseline.get(rel)
219
220
  if exp is None and real is not None:
220
- changes[rel] = "deleted"
221
+ # A file missing from the export AND from the baseline was added
222
+ # host-side after materialisation - never an agent deletion, so it
223
+ # must not be unlinked (040).
224
+ if base is None:
225
+ changes[rel] = "host-added"
226
+ else:
227
+ changes[rel] = "deleted"
221
228
  elif real is None and exp is not None:
222
229
  changes[rel] = "added"
223
230
  elif exp != real:
@@ -250,6 +257,8 @@ def sync_back(export_root: str | Path, real_repo: str | Path,
250
257
  plan_changes[rel] = kind
251
258
 
252
259
  plan = plan_sync_back(plan_changes, declared_rw, cache_paths, excludes)
260
+ host_added_paths = tuple(sorted(
261
+ rel for rel, kind in changes.items() if kind == "host-added"))
253
262
  merged_paths = tuple(sorted(merged_paths))
254
263
  hard_conflicts = tuple(sorted(hard_conflicts))
255
264
 
@@ -269,6 +278,7 @@ def sync_back(export_root: str | Path, real_repo: str | Path,
269
278
  return SyncBackReport(
270
279
  synced_paths=(), conflicts=hard_conflicts,
271
280
  merged_paths=merged_paths, lane_violations=lane_violations,
281
+ host_added_paths=host_added_paths,
272
282
  step_id=step_id, timestamp=datetime.now(timezone.utc).isoformat())
273
283
 
274
284
  # A hard (non-merge) conflict keeps the BR-4.4 loud failure and the
@@ -278,8 +288,8 @@ def sync_back(export_root: str | Path, real_repo: str | Path,
278
288
  if hard_conflicts:
279
289
  return SyncBackReport(
280
290
  synced_paths=(), conflicts=hard_conflicts,
281
- merged_paths=merged_paths, step_id=step_id,
282
- timestamp=datetime.now(timezone.utc).isoformat())
291
+ merged_paths=merged_paths, host_added_paths=host_added_paths,
292
+ step_id=step_id, timestamp=datetime.now(timezone.utc).isoformat())
283
293
 
284
294
  for rel in plan.to_copy:
285
295
  src = export_root / rel
@@ -292,4 +302,5 @@ def sync_back(export_root: str | Path, real_repo: str | Path,
292
302
  return SyncBackReport(
293
303
  synced_paths=plan.to_copy + plan.to_delete,
294
304
  conflicts=(), merged_paths=merged_paths, lane_violations=(),
305
+ host_added_paths=host_added_paths,
295
306
  step_id=step_id, timestamp=datetime.now(timezone.utc).isoformat())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a34
3
+ Version: 1.0.0a35
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
@@ -1585,6 +1585,38 @@ class TestUnit5RuntimeEnforcement:
1585
1585
  finally:
1586
1586
  backend.close()
1587
1587
 
1588
+ def test_sync_back_host_added_skipped_and_audited(self, tmp_path):
1589
+ """040: a real file absent from both the export and the baseline is a
1590
+ host-side addition, not an agent deletion - it is left untouched and
1591
+ the sync-back audit record names the skipped path."""
1592
+ import hashlib
1593
+
1594
+ ws = tmp_path / "ws"
1595
+ (ws / "src").mkdir(parents=True)
1596
+ (ws / "src" / "a.py").write_text("orig")
1597
+ export = tmp_path / "export"
1598
+ (export / "src").mkdir(parents=True)
1599
+ (export / "src" / "a.py").write_text("orig")
1600
+ (ws / "src" / "host-added.py").write_text("host-side addition")
1601
+
1602
+ runner = FakeRunner(ok("cid"), ok(""))
1603
+ backend = _backend(tmp_path, runner)
1604
+ backend._export_root = str(export)
1605
+ backend._baseline = {"src/a.py": hashlib.sha256(b"orig").hexdigest()}
1606
+ ms = resolve_mount_set([MountSpec(path="src", mode="rw")])
1607
+ req = SandboxExecutionRequest(argv=["echo", "hi"], cwd=str(export),
1608
+ env={}, network=False, mount_set=ms,
1609
+ egress_policy=EgressPolicy())
1610
+ try:
1611
+ backend.run(req)
1612
+ assert (ws / "src" / "host-added.py").read_text() == "host-side addition"
1613
+ audit = (tmp_path / "state" / "sandbox-audit.jsonl").read_text()
1614
+ assert "sync_back" in audit
1615
+ assert "host_added_paths" in audit
1616
+ assert "src/host-added.py" in audit
1617
+ finally:
1618
+ backend.close()
1619
+
1588
1620
  def test_whole_tree_rw_syncs_back_and_cache_untouched(self, tmp_path):
1589
1621
  """BR-4 + BR-3/F2: a ``.``-rw step (the shipped pipeline's shape)
1590
1622
  syncs its edits back, while the bind-mounted cache is never classified
@@ -348,3 +348,49 @@ class TestMergeOnConflictPartition:
348
348
  assert report.merged_paths == ()
349
349
  assert "src/audit.md" in report.synced_paths
350
350
  assert (ws / "src" / "audit.md").read_text() == "base\nexport\n"
351
+
352
+
353
+ class TestDeletedBranchBaselineGuard:
354
+ """040: the deleted branch consults the baseline before unlinking. A real
355
+ file absent from the baseline and the export is a host-side addition, never
356
+ an agent deletion; a baseline-present file deleted in the export still
357
+ mirrors (unlinks) exactly as before."""
358
+
359
+ def test_baseline_present_export_deleted_still_unlinks(self, tmp_path):
360
+ ws = _workspace(tmp_path)
361
+ exp = _export(tmp_path, ws)
362
+ (exp / "src" / "b.py").unlink()
363
+ baseline = {"src/a.py": _digest(ws / "src" / "a.py"),
364
+ "src/b.py": _digest(ws / "src" / "b.py")}
365
+ report = sync_back(exp, ws, ["src"], baseline=baseline)
366
+ assert report.conflicts == ()
367
+ assert report.host_added_paths == ()
368
+ assert "src/b.py" in report.synced_paths
369
+ assert not (ws / "src" / "b.py").exists()
370
+
371
+ def test_baseline_absent_real_only_file_is_host_added_and_untouched(
372
+ self, tmp_path):
373
+ ws = _workspace(tmp_path)
374
+ exp = _export(tmp_path, ws)
375
+ (ws / "src" / "host-added.py").write_text("host-side addition")
376
+ baseline = {"src/a.py": _digest(ws / "src" / "a.py"),
377
+ "src/b.py": _digest(ws / "src" / "b.py")}
378
+ report = sync_back(exp, ws, ["src"], baseline=baseline)
379
+ assert report.conflicts == ()
380
+ assert report.host_added_paths == ("src/host-added.py",)
381
+ assert report.synced_paths == ()
382
+ assert (ws / "src" / "host-added.py").read_text() == "host-side addition"
383
+
384
+ def test_modified_branch_hard_conflict_unchanged(self, tmp_path):
385
+ """Regression: BR-4.4 still treats a baseline-present path changed on
386
+ both sides as a loud conflict, never an overwrite."""
387
+ ws = _workspace(tmp_path)
388
+ exp = _export(tmp_path, ws)
389
+ (exp / "src" / "a.py").write_text("agent-change")
390
+ (ws / "src" / "a.py").write_text("external-change")
391
+ baseline = {"src/a.py": hashlib.sha256(b"orig-a").hexdigest()}
392
+ report = sync_back(exp, ws, ["src"], baseline=baseline)
393
+ assert report.conflicts == ("src/a.py",)
394
+ assert report.host_added_paths == ()
395
+ assert report.synced_paths == ()
396
+ assert (ws / "src" / "a.py").read_text() == "external-change"