agentltl-coding 0.2.4__tar.gz → 0.2.5__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 (55) hide show
  1. {agentltl_coding-0.2.4/src/agentltl_coding.egg-info → agentltl_coding-0.2.5}/PKG-INFO +9 -3
  2. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/README.md +7 -1
  3. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/pyproject.toml +2 -2
  4. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/__init__.py +1 -1
  5. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/pattern.py +13 -5
  6. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/store.py +42 -3
  7. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5/src/agentltl_coding.egg-info}/PKG-INFO +9 -3
  8. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding.egg-info/SOURCES.txt +2 -1
  9. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding.egg-info/requires.txt +1 -1
  10. agentltl_coding-0.2.5/tests/test_windows.py +61 -0
  11. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/LICENSE +0 -0
  12. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/setup.cfg +0 -0
  13. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/cli.py +0 -0
  14. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/edit.py +0 -0
  15. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/guard.py +0 -0
  16. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/harness.py +0 -0
  17. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/ask-before-creating-credentials.yaml +0 -0
  18. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/ask-before-discarding-work.yaml +0 -0
  19. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/ask-before-infra-changes.yaml +0 -0
  20. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/ask-before-installing.yaml +0 -0
  21. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/ask-before-reading-secret-stores.yaml +0 -0
  22. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/ask-before-recursive-delete.yaml +0 -0
  23. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/devops-secrets.yaml +0 -0
  24. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-claude-coauthor.yaml +0 -0
  25. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-codex-coauthor.yaml +0 -0
  26. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-copilot-coauthor.yaml +0 -0
  27. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-env-dumps.yaml +0 -0
  28. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-force-push.yaml +0 -0
  29. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-leaky-debug.yaml +0 -0
  30. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-push-to-main.yaml +0 -0
  31. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-secrets-in-commands.yaml +0 -0
  32. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-skipping-secret-scans.yaml +0 -0
  33. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/no-vibe-coauthor.yaml +0 -0
  34. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/protect-env-files.yaml +0 -0
  35. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/protect-secret-files.yaml +0 -0
  36. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/read-before-overwrite.yaml +0 -0
  37. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/subagents-on-sonnet.yaml +0 -0
  38. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/tests-before-finishing.yaml +0 -0
  39. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/library/tests-before-push.yaml +0 -0
  40. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/memory.py +0 -0
  41. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/rules.py +0 -0
  42. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/scan.py +0 -0
  43. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding/session.py +0 -0
  44. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding.egg-info/dependency_links.txt +0 -0
  45. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding.egg-info/entry_points.txt +0 -0
  46. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/src/agentltl_coding.egg-info/top_level.txt +0 -0
  47. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_finish.py +0 -0
  48. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_golden.py +0 -0
  49. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_guard.py +0 -0
  50. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_harness.py +0 -0
  51. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_library.py +0 -0
  52. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_rules.py +0 -0
  53. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_scan.py +0 -0
  54. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_scripted_model.py +0 -0
  55. {agentltl_coding-0.2.4 → agentltl_coding-0.2.5}/tests/test_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentltl-coding
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: AgentLTL rules for coding agents: the AGENTLTL.yaml language, a rule library, and a guard any harness can call.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://agentltl.github.io
@@ -9,7 +9,7 @@ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: agentltl>=0.2
12
- Requires-Dist: cli-to-tools>=0.2
12
+ Requires-Dist: cli-to-tools>=0.2.2
13
13
  Requires-Dist: pyyaml>=6.0
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -58,7 +58,13 @@ plugin's README into your agent ([Claude Code](https://github.com/AgentLTL/agent
58
58
  harness for another agent, paste this into your coding agent:
59
59
 
60
60
  ```
61
- Install agentltl-coding (pip install "git+https://github.com/AgentLTL/agentltl-coding.git" in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
61
+ Install agentltl-coding (pip install agentltl-coding, in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
62
+ ```
63
+
64
+ Or install it yourself:
65
+
66
+ ```bash
67
+ pip install agentltl-coding # also installs agentltl and cli-to-tools
62
68
  ```
63
69
 
64
70
  ## A harness in a few lines
@@ -40,7 +40,13 @@ plugin's README into your agent ([Claude Code](https://github.com/AgentLTL/agent
40
40
  harness for another agent, paste this into your coding agent:
41
41
 
42
42
  ```
43
- Install agentltl-coding (pip install "git+https://github.com/AgentLTL/agentltl-coding.git" in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
43
+ Install agentltl-coding (pip install agentltl-coding, in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
44
+ ```
45
+
46
+ Or install it yourself:
47
+
48
+ ```bash
49
+ pip install agentltl-coding # also installs agentltl and cli-to-tools
44
50
  ```
45
51
 
46
52
  ## A harness in a few lines
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentltl-coding"
7
- version = "0.2.4"
7
+ version = "0.2.5"
8
8
  description = "AgentLTL rules for coding agents: the AGENTLTL.yaml language, a rule library, and a guard any harness can call."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
12
  dependencies = [
13
13
  "agentltl>=0.2",
14
- "cli-to-tools>=0.2",
14
+ "cli-to-tools>=0.2.2",
15
15
  "pyyaml>=6.0",
16
16
  ]
17
17
 
@@ -15,7 +15,7 @@ from .pattern import Paths
15
15
  from .rules import RuleFileError, RuleSet, library, load, loads, rule_files
16
16
  from .session import Session
17
17
 
18
- __version__ = "0.2.4"
18
+ __version__ = "0.2.5"
19
19
 
20
20
  __all__ = ["Guard", "Harness", "Paths", "RuleFileError", "RuleSet", "Session", "Verdict", "configure",
21
21
  "current", "library", "lint", "load", "loads", "rule_files", "translator_for"]
@@ -44,6 +44,7 @@ from __future__ import annotations
44
44
 
45
45
  import fnmatch
46
46
  import os
47
+ import posixpath
47
48
  from dataclasses import dataclass, field
48
49
  from typing import Any, Dict, List, Optional, Sequence, Tuple
49
50
 
@@ -274,15 +275,22 @@ def _glob(value: str, pattern: str, paths: Paths) -> bool:
274
275
  return False
275
276
 
276
277
 
278
+ def _slashes(path: str) -> str:
279
+ """*path* with ``/`` separators: globs are written with ``/``, and on Windows paths come
280
+ with ``\\``."""
281
+ return path.replace("\\", "/") if os.sep == "\\" else path
282
+
283
+
277
284
  def _forms(value: str, paths: Paths, basename: bool) -> Sequence[str]:
278
- forms = [value]
285
+ forms = [_slashes(value)]
279
286
  if value and not value.startswith(("-", "http://", "https://")):
280
- absolute = _absolute(value, paths)
287
+ absolute = _slashes(_absolute(value, paths))
288
+ root = _slashes(paths.root).rstrip("/")
281
289
  forms.append(absolute)
282
- if paths.root and (absolute == paths.root or absolute.startswith(paths.root + os.sep)):
283
- forms.append(os.path.relpath(absolute, paths.root))
290
+ if root and (absolute == root or absolute.startswith(root + "/")):
291
+ forms.append(absolute[len(root):].lstrip("/") or ".")
284
292
  if basename:
285
- forms.append(os.path.basename(absolute))
293
+ forms.append(posixpath.basename(absolute))
286
294
  return forms
287
295
 
288
296
 
@@ -14,16 +14,55 @@ environment), else ``~/.cache/agentltl-<harness>/sessions``.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import fcntl
18
17
  import hashlib
19
18
  import json
20
19
  import os
21
20
  import re
22
21
  import sys
23
22
  import tempfile
23
+ import time
24
24
  from contextlib import contextmanager
25
25
  from typing import Any, Dict, Iterator, List, Optional
26
26
 
27
+ if os.name == "nt":
28
+ import errno
29
+ import msvcrt
30
+
31
+ _CONTENTION = {errno.EACCES, errno.EDEADLK, getattr(errno, "EDEADLOCK", errno.EDEADLK)}
32
+
33
+ def _lock_exclusive(fh) -> None:
34
+ """Block until this process holds an exclusive lock on *fh*; closing it releases it."""
35
+ deadline = time.monotonic() + 60
36
+ while True:
37
+ try:
38
+ fh.seek(0)
39
+ msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
40
+ return
41
+ except OSError as exc:
42
+ # Another process holds the lock (EACCES, or EDEADLOCK after msvcrt's own
43
+ # retries): wait. Any other error cannot recover, so surface it now.
44
+ if exc.errno not in _CONTENTION or time.monotonic() > deadline:
45
+ raise
46
+ time.sleep(0.05)
47
+ else:
48
+ import fcntl
49
+
50
+ def _lock_exclusive(fh) -> None:
51
+ """Block until this process holds an exclusive lock on *fh*; closing it releases it."""
52
+ fcntl.flock(fh, fcntl.LOCK_EX)
53
+
54
+
55
+ def _replace(tmp: str, path: str) -> None:
56
+ """os.replace, retrying on Windows, which refuses while another process reads *path*."""
57
+ for attempt in range(100):
58
+ try:
59
+ os.replace(tmp, path)
60
+ return
61
+ except PermissionError:
62
+ if os.name != "nt" or attempt == 99:
63
+ raise
64
+ time.sleep(0.02)
65
+
27
66
 
28
67
  def state_dir() -> str:
29
68
  from .harness import current
@@ -67,13 +106,13 @@ def read_project(project_dir: str) -> Dict[str, Any]:
67
106
  def _locked(path: str) -> Iterator[Dict[str, Any]]:
68
107
  os.makedirs(os.path.dirname(path), exist_ok=True)
69
108
  with open(path + ".lock", "w") as lock:
70
- fcntl.flock(lock, fcntl.LOCK_EX)
109
+ _lock_exclusive(lock)
71
110
  state = _read(path)
72
111
  yield state
73
112
  fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), suffix=".tmp")
74
113
  with os.fdopen(fd, "w", encoding="utf-8") as fh:
75
114
  json.dump(state, fh, default=str)
76
- os.replace(tmp, path)
115
+ _replace(tmp, path)
77
116
 
78
117
 
79
118
  @contextmanager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentltl-coding
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: AgentLTL rules for coding agents: the AGENTLTL.yaml language, a rule library, and a guard any harness can call.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://agentltl.github.io
@@ -9,7 +9,7 @@ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: agentltl>=0.2
12
- Requires-Dist: cli-to-tools>=0.2
12
+ Requires-Dist: cli-to-tools>=0.2.2
13
13
  Requires-Dist: pyyaml>=6.0
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -58,7 +58,13 @@ plugin's README into your agent ([Claude Code](https://github.com/AgentLTL/agent
58
58
  harness for another agent, paste this into your coding agent:
59
59
 
60
60
  ```
61
- Install agentltl-coding (pip install "git+https://github.com/AgentLTL/agentltl-coding.git" in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
61
+ Install agentltl-coding (pip install agentltl-coding, in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
62
+ ```
63
+
64
+ Or install it yourself:
65
+
66
+ ```bash
67
+ pip install agentltl-coding # also installs agentltl and cli-to-tools
62
68
  ```
63
69
 
64
70
  ## A harness in a few lines
@@ -49,4 +49,5 @@ tests/test_library.py
49
49
  tests/test_rules.py
50
50
  tests/test_scan.py
51
51
  tests/test_scripted_model.py
52
- tests/test_session.py
52
+ tests/test_session.py
53
+ tests/test_windows.py
@@ -1,5 +1,5 @@
1
1
  agentltl>=0.2
2
- cli-to-tools>=0.2
2
+ cli-to-tools>=0.2.2
3
3
  pyyaml>=6.0
4
4
 
5
5
  [dev]
@@ -0,0 +1,61 @@
1
+ """What only differs on Windows: backslash paths against `/` globs, and the session lock."""
2
+
3
+ import errno
4
+ import os
5
+ import time
6
+
7
+ import pytest
8
+
9
+ from agentltl_coding.pattern import Paths, parse_target
10
+
11
+ windows_only = pytest.mark.skipif(os.name != "nt", reason="Windows paths and msvcrt locking")
12
+
13
+
14
+ @windows_only
15
+ @pytest.mark.parametrize("glob,value,expected", [
16
+ ("src/*.py", "a.py", True), # project-relative glob, relative argument
17
+ ("src/*.py", "C:\\proj\\src\\a.py", True), # ... and an absolute backslash argument
18
+ ("src/*.py", "..\\a.py", False),
19
+ ("*.lock", "C:\\proj\\sub\\poetry.lock", True), # basename glob
20
+ ("**/.env", "C:\\proj\\.env", True),
21
+ ("**/.env", "C:\\proj\\.environment", False),
22
+ ])
23
+ def test_globs_match_backslash_paths(glob, value, expected):
24
+ paths = Paths("C:\\proj\\src", "C:\\proj")
25
+ target = parse_target({"tool": "rm", "where": {"paths": glob}}, "t")
26
+ assert target.matches("rm", {"paths": [value]}, paths) is expected
27
+
28
+
29
+ @windows_only
30
+ def test_the_session_lock_surfaces_an_error_that_cannot_recover(tmp_path, monkeypatch):
31
+ import msvcrt
32
+
33
+ from agentltl_coding import store
34
+
35
+ def invalid(*args):
36
+ raise OSError(errno.EINVAL, "invalid lock")
37
+
38
+ monkeypatch.setattr(msvcrt, "locking", invalid)
39
+ start = time.monotonic()
40
+ with open(tmp_path / "x.lock", "w") as fh, pytest.raises(OSError):
41
+ store._lock_exclusive(fh)
42
+ assert time.monotonic() - start < 5 # not the 60 seconds of waiting for a holder
43
+
44
+
45
+ @windows_only
46
+ def test_the_session_lock_waits_while_another_process_holds_it(tmp_path, monkeypatch):
47
+ import msvcrt
48
+
49
+ from agentltl_coding import store
50
+
51
+ calls = []
52
+
53
+ def busy_twice(fd, mode, nbytes):
54
+ calls.append(1)
55
+ if len(calls) < 3:
56
+ raise OSError(errno.EACCES, "locked")
57
+
58
+ monkeypatch.setattr(msvcrt, "locking", busy_twice)
59
+ with open(tmp_path / "x.lock", "w") as fh:
60
+ store._lock_exclusive(fh)
61
+ assert len(calls) == 3
File without changes