cluxion-agentplugin-supercoder 0.2.6__tar.gz → 0.2.7__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 (64) hide show
  1. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/pyproject.toml +1 -1
  3. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/__init__.py +1 -1
  4. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/hash_patch.py +62 -23
  5. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_hash_patch.py +83 -0
  6. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/uv.lock +1 -1
  7. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/.github/workflows/ci.yml +0 -0
  8. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/.github/workflows/publish.yml +0 -0
  9. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/.gitignore +0 -0
  10. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/ARCHITECTURE.md +0 -0
  11. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/README.md +0 -0
  12. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/agent-surfaces.md +0 -0
  13. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/architecture.md +0 -0
  14. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/capabilities.md +0 -0
  15. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/design.md +0 -0
  16. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/installation.md +0 -0
  17. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/rust-architecture.md +0 -0
  18. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/Docs/tools.md +0 -0
  19. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/LICENSE +0 -0
  20. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/README.md +0 -0
  21. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/__init__.py +0 -0
  22. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/adapters/claude/.claude-plugin/plugin.json +0 -0
  23. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/adapters/claude/skills/supercoder/SKILL.md +0 -0
  24. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/adapters/codex/config-snippet.toml +0 -0
  25. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/adapters/hermes/README.md +0 -0
  26. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/plugin.yaml +0 -0
  27. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/Cargo.lock +0 -0
  28. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/Cargo.toml +0 -0
  29. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/pyproject.toml +0 -0
  30. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/lib.rs +0 -0
  31. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/main.rs +0 -0
  32. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/outline.rs +0 -0
  33. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/rust/supercoder_index/src/syntax.rs +0 -0
  34. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/scripts/repack_native_wheel.py +0 -0
  35. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/cli.py +0 -0
  36. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/cursor.py +0 -0
  37. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/line_budget.py +0 -0
  38. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/lint_gate.py +0 -0
  39. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/queue.py +0 -0
  40. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/repo_map.py +0 -0
  41. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/retry_loop.py +0 -0
  42. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/safety.py +0 -0
  43. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/syntax_gate.py +0 -0
  44. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/core/test_gate.py +0 -0
  45. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/__init__.py +0 -0
  46. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/catalog.json +0 -0
  47. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/framework.py +0 -0
  48. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/doctor/probes.py +0 -0
  49. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/plugin.py +0 -0
  50. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/runner.py +0 -0
  51. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/rust_bridge.py +0 -0
  52. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/src/cluxion_agentplugin_supercoder/schemas.py +0 -0
  53. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_cursor.py +0 -0
  54. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_doctor.py +0 -0
  55. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_line_budget.py +0 -0
  56. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_lint_gate.py +0 -0
  57. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_plugin.py +0 -0
  58. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_queue.py +0 -0
  59. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_repo_map.py +0 -0
  60. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_retry_loop.py +0 -0
  61. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_rust_bridge.py +0 -0
  62. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_safety.py +0 -0
  63. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_syntax_gate.py +0 -0
  64. {cluxion_agentplugin_supercoder-0.2.6 → cluxion_agentplugin_supercoder-0.2.7}/tests/test_test_gate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluxion-agentplugin-supercoder
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates.
5
5
  Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-supercoder
6
6
  Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-supercoder
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cluxion-agentplugin-supercoder"
7
- version = "0.2.6"
7
+ version = "0.2.7"
8
8
  description = "Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.6"
3
+ __version__ = "0.2.7"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -3,15 +3,40 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
+ import os
7
+ import tempfile
8
+ from contextlib import contextmanager
6
9
  from dataclasses import dataclass
7
10
  from difflib import SequenceMatcher
8
11
  from pathlib import Path
9
12
 
13
+ try:
14
+ import fcntl
15
+ except ImportError:
16
+ fcntl = None # type: ignore[assignment]
17
+
10
18
  DEFAULT_FUZZY_THRESHOLD = 0.86
11
19
  MAX_CONTEXT_SCAN = 8
12
20
  MAX_LINE_DRIFT = 2
13
21
 
14
22
 
23
+ @contextmanager
24
+ def _exclusive_lock(path: Path):
25
+ """Exclusive advisory lock on target file (fcntl.flock). Graceful degrade on non-POSIX."""
26
+ if fcntl is None or not path.exists():
27
+ yield
28
+ return
29
+ fd = os.open(str(path), os.O_RDWR)
30
+ try:
31
+ fcntl.flock(fd, fcntl.LOCK_EX)
32
+ yield
33
+ finally:
34
+ try:
35
+ fcntl.flock(fd, fcntl.LOCK_UN)
36
+ finally:
37
+ os.close(fd)
38
+
39
+
15
40
  @dataclass(frozen=True, slots=True)
16
41
  class PatchResult:
17
42
  success: bool
@@ -44,28 +69,29 @@ def apply_patch(
44
69
  ) -> PatchResult:
45
70
  if not path.exists():
46
71
  return _failed(str(path), "missing_file", expected_file_hash, "file not found")
47
- text = path.read_text(encoding="utf-8")
48
- current_hash = file_hash(text)
49
- if expected_file_hash and current_hash != _normalize_hash(expected_file_hash):
50
- return _failed(str(path), "stale_file", expected_file_hash, "file changed since cursor was created")
51
- exact = _exact_spans(text, old_text)
52
- if exact:
53
- start, end = exact[0]
54
- return _commit(path, text, start, end, new_text, "exact", expected_file_hash or current_hash, current_hash, 1.0)
55
- fuzzy = _best_fuzzy_span(text, old_text)
56
- if fuzzy and fuzzy[3] >= fuzzy_threshold and not fuzzy[4]:
57
- return _commit(
58
- path,
59
- text,
60
- fuzzy[0],
61
- fuzzy[1],
62
- new_text,
63
- "fuzzy",
64
- expected_file_hash or current_hash,
65
- current_hash,
66
- fuzzy[3],
67
- )
68
- return _failed(str(path), "no_match", expected_file_hash or current_hash, "patch target not found")
72
+ with _exclusive_lock(path):
73
+ text = path.read_text(encoding="utf-8")
74
+ current_hash = file_hash(text)
75
+ if expected_file_hash and current_hash != _normalize_hash(expected_file_hash):
76
+ return _failed(str(path), "stale_file", expected_file_hash, "file changed since cursor was created")
77
+ exact = _exact_spans(text, old_text)
78
+ if exact:
79
+ start, end = exact[0]
80
+ return _commit(path, text, start, end, new_text, "exact", expected_file_hash or current_hash, current_hash, 1.0)
81
+ fuzzy = _best_fuzzy_span(text, old_text)
82
+ if fuzzy and fuzzy[3] >= fuzzy_threshold and not fuzzy[4]:
83
+ return _commit(
84
+ path,
85
+ text,
86
+ fuzzy[0],
87
+ fuzzy[1],
88
+ new_text,
89
+ "fuzzy",
90
+ expected_file_hash or current_hash,
91
+ current_hash,
92
+ fuzzy[3],
93
+ )
94
+ return _failed(str(path), "no_match", expected_file_hash or current_hash, "patch target not found")
69
95
 
70
96
 
71
97
  def _normalize_newlines(content: str) -> str:
@@ -143,6 +169,19 @@ def _best_fuzzy_span(text: str, reference: str) -> tuple[int, int, str, float, b
143
169
  return best[0], best[1], best[2], best[3], ambiguous
144
170
 
145
171
 
172
+ def _atomic_write(path: Path, content: str) -> None:
173
+ """Atomic replace via temp in same dir + fsync to prevent corruption on crash."""
174
+ dir_ = path.parent
175
+ with tempfile.NamedTemporaryFile(
176
+ mode="w", encoding="utf-8", dir=dir_, delete=False, suffix=".tmp"
177
+ ) as tmp:
178
+ tmp.write(content)
179
+ tmp.flush()
180
+ os.fsync(tmp.fileno())
181
+ tmp_path = Path(tmp.name)
182
+ os.replace(tmp_path, path)
183
+
184
+
146
185
  def _commit(
147
186
  path: Path,
148
187
  text: str,
@@ -155,7 +194,7 @@ def _commit(
155
194
  score: float,
156
195
  ) -> PatchResult:
157
196
  updated = f"{text[:start]}{new_content}{text[end:]}"
158
- path.write_text(updated, encoding="utf-8")
197
+ _atomic_write(path, updated)
159
198
  return PatchResult(True, str(path), strategy, "patch applied", expected, matched, round(score, 4), 1)
160
199
 
161
200
 
@@ -1,5 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import concurrent.futures
4
+ import contextlib
5
+ import os
6
+ import tempfile
3
7
  from pathlib import Path
4
8
 
5
9
  import pytest
@@ -116,3 +120,82 @@ def test_duplicate_blocks_still_ambiguous(tmp_path: Path) -> None:
116
120
  result = apply_patch(path, old_text=near, new_text="def f():\n return 3\n")
117
121
  assert result.success is False
118
122
  assert result.strategy == "no_match"
123
+
124
+
125
+ # === Concurrency and atomicity tests ===
126
+
127
+ def _worker_apply(i: int, path: Path) -> bool:
128
+ """Worker that applies a unique non-overlapping patch under lock."""
129
+ old = f"# UNIQUE_PATCH_{i}_START\n"
130
+ new = f"# UNIQUE_PATCH_{i}_DONE\n"
131
+ # distinct markers, no value chaining conflict
132
+ result = apply_patch(path, old_text=old, new_text=new)
133
+ return result.success
134
+
135
+
136
+ def test_concurrent_patches_no_lost_update(tmp_path: Path) -> None:
137
+ """8 concurrent workers on same file: lock serializes, no lost updates, final coherent."""
138
+ path = tmp_path / "concurrent.py"
139
+ # initial content with 8 distinct unique markers
140
+ initial = "\n".join(f"# UNIQUE_PATCH_{i}_START" for i in range(8)) + "\n"
141
+ path.write_text(initial, encoding="utf-8")
142
+
143
+ # each patch changes its unique marker; lock ensures serialization, all succeed
144
+ with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
145
+ futures = [
146
+ executor.submit(_worker_apply, i, path) for i in range(8)
147
+ ]
148
+ successes = [f.result() for f in concurrent.futures.as_completed(futures)]
149
+
150
+ assert all(successes), "Some patches lost due to race"
151
+ final = path.read_text(encoding="utf-8")
152
+ # final should have all DONE markers, no START left
153
+ for i in range(8):
154
+ assert f"# UNIQUE_PATCH_{i}_DONE" in final
155
+ assert f"# UNIQUE_PATCH_{i}_START" not in final
156
+
157
+ # hash chain consistent (changed from initial)
158
+ new_hash = file_hash(final)
159
+ assert new_hash != file_hash(initial)
160
+
161
+
162
+ def test_atomic_write_interruption_leaves_original_intact(tmp_path: Path) -> None:
163
+ """Simulated mid-write crash leaves ORIGINAL file intact (temp may remain, no truncate)."""
164
+ path = tmp_path / "atomic_test.txt"
165
+ original = "IMPORTANT ORIGINAL CONTENT\nDO NOT LOSE\n"
166
+ path.write_text(original, encoding="utf-8")
167
+ orig_hash = file_hash(original)
168
+
169
+ # simulate crash mid atomic write by patching _atomic_write temporarily
170
+ def crashing_atomic(p: Path, content: str) -> None:
171
+ dir_ = p.parent
172
+ with tempfile.NamedTemporaryFile(
173
+ mode="w", encoding="utf-8", dir=dir_, delete=False, suffix=".tmp"
174
+ ) as tmp:
175
+ tmp.write(content[:10]) # partial write
176
+ tmp.flush()
177
+ os.fsync(tmp.fileno())
178
+ # simulate kill before replace
179
+ raise RuntimeError("simulated crash mid-write")
180
+
181
+ original_atomic = None
182
+ try:
183
+ # monkey patch for test
184
+ import cluxion_agentplugin_supercoder.core.hash_patch as hp
185
+
186
+ original_atomic = hp._atomic_write
187
+ hp._atomic_write = crashing_atomic
188
+ with contextlib.suppress(RuntimeError):
189
+ hp._atomic_write(path, "CORRUPTED NEW CONTENT\n")
190
+ # after crash, original must be untouched
191
+ after = path.read_text(encoding="utf-8")
192
+ assert after == original, "Atomic write failed: original was corrupted or truncated"
193
+ assert file_hash(after) == orig_hash
194
+ finally:
195
+ import cluxion_agentplugin_supercoder.core.hash_patch as hp
196
+
197
+ if original_atomic is not None:
198
+ hp._atomic_write = original_atomic
199
+ # cleanup any leftover temp if test created
200
+ for f in tmp_path.glob("*.tmp"):
201
+ f.unlink(missing_ok=True)
@@ -160,7 +160,7 @@ wheels = [
160
160
 
161
161
  [[package]]
162
162
  name = "cluxion-agentplugin-supercoder"
163
- version = "0.2.6"
163
+ version = "0.2.7"
164
164
  source = { editable = "." }
165
165
  dependencies = [
166
166
  { name = "psutil" },