cluxion-agentplugin-supercoder 0.2.9__tar.gz → 0.2.11__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.9 → cluxion_agentplugin_supercoder-0.2.11}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/plugin.yaml +1 -1
  3. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/pyproject.toml +1 -1
  4. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/hash_patch.py +13 -4
  5. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/line_budget.py +28 -17
  6. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/repo_map.py +9 -4
  7. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/doctor/probes.py +55 -0
  8. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/rust_bridge.py +29 -9
  9. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_doctor.py +76 -0
  10. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_hash_patch.py +18 -0
  11. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_line_budget.py +13 -0
  12. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_repo_map.py +11 -0
  13. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/uv.lock +1 -1
  14. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/.github/workflows/ci.yml +0 -0
  15. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/.github/workflows/publish.yml +0 -0
  16. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/.gitignore +0 -0
  17. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/ARCHITECTURE.md +0 -0
  18. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/README.md +0 -0
  19. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/agent-surfaces.md +0 -0
  20. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/architecture.md +0 -0
  21. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/capabilities.md +0 -0
  22. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/design.md +0 -0
  23. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/installation.md +0 -0
  24. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/rust-architecture.md +0 -0
  25. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/Docs/tools.md +0 -0
  26. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/LICENSE +0 -0
  27. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/README.md +0 -0
  28. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/__init__.py +0 -0
  29. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/adapters/claude/.claude-plugin/plugin.json +0 -0
  30. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/adapters/claude/skills/supercoder/SKILL.md +0 -0
  31. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/adapters/codex/config-snippet.toml +0 -0
  32. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/adapters/hermes/README.md +0 -0
  33. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/Cargo.lock +0 -0
  34. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/Cargo.toml +0 -0
  35. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/pyproject.toml +0 -0
  36. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/src/lib.rs +0 -0
  37. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/src/main.rs +0 -0
  38. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/src/outline.rs +0 -0
  39. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/rust/supercoder_index/src/syntax.rs +0 -0
  40. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/scripts/repack_native_wheel.py +0 -0
  41. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/__init__.py +0 -0
  42. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/cli.py +0 -0
  43. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/cursor.py +0 -0
  44. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/lint_gate.py +0 -0
  45. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/queue.py +0 -0
  46. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/retry_loop.py +0 -0
  47. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/safety.py +0 -0
  48. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/syntax_gate.py +0 -0
  49. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/core/test_gate.py +0 -0
  50. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/doctor/__init__.py +0 -0
  51. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/doctor/catalog.json +0 -0
  52. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/doctor/framework.py +0 -0
  53. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/plugin.py +0 -0
  54. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/runner.py +0 -0
  55. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/src/cluxion_agentplugin_supercoder/schemas.py +0 -0
  56. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_cursor.py +0 -0
  57. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_lint_gate.py +0 -0
  58. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_plugin.py +0 -0
  59. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_queue.py +0 -0
  60. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_retry_loop.py +0 -0
  61. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_rust_bridge.py +0 -0
  62. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_safety.py +0 -0
  63. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/tests/test_syntax_gate.py +0 -0
  64. {cluxion_agentplugin_supercoder-0.2.9 → cluxion_agentplugin_supercoder-0.2.11}/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.9
3
+ Version: 0.2.11
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
@@ -1,5 +1,5 @@
1
1
  name: cluxion-agentplugin-supercoder
2
- version: 0.2.2
2
+ version: 0.2.11
3
3
  description: "Universal agent coding harness: cursor, safe patch, line budget, Rust index."
4
4
  author: cluxion
5
5
  kind: standalone
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cluxion-agentplugin-supercoder"
7
- version = "0.2.9"
7
+ version = "0.2.11"
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"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import os
7
7
  import tempfile
8
+ import threading
8
9
  from contextlib import contextmanager
9
10
  from dataclasses import dataclass
10
11
  from difflib import SequenceMatcher
@@ -19,14 +20,22 @@ DEFAULT_FUZZY_THRESHOLD = 0.86
19
20
  MAX_CONTEXT_SCAN = 8
20
21
  MAX_LINE_DRIFT = 2
21
22
 
23
+ _thread_fallback_lock = threading.Lock()
24
+
25
+
26
+ def _lock_path(path: Path) -> Path:
27
+ return path.parent / f".{path.name}.cluxion-lock"
28
+
22
29
 
23
30
  @contextmanager
24
31
  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
32
+ """Exclusive advisory lock on stable sidecar file (fcntl.flock). Graceful degrade on non-POSIX."""
33
+ if fcntl is None:
34
+ with _thread_fallback_lock:
35
+ yield
28
36
  return
29
- fd = os.open(str(path), os.O_RDWR)
37
+ lock_path = _lock_path(path)
38
+ fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o600)
30
39
  try:
31
40
  fcntl.flock(fd, fcntl.LOCK_EX)
32
41
  yield
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  from dataclasses import dataclass
6
7
 
7
8
 
@@ -32,25 +33,35 @@ def budget_for(mode: str, *, requested_lines: int, remaining: int = 10_000) -> B
32
33
  return BudgetDecision(True, "within_budget", cap, remaining - requested_lines)
33
34
 
34
35
 
36
+ _ASCII_CODING_KEYWORDS = (
37
+ "code",
38
+ "fix",
39
+ "implement",
40
+ "refactor",
41
+ "patch",
42
+ "test",
43
+ "bug",
44
+ "debug",
45
+ )
46
+ _KOREAN_CODING_KEYWORDS = (
47
+ "코드",
48
+ "수정",
49
+ "구현",
50
+ "리팩터",
51
+ "패치",
52
+ "테스트",
53
+ "버그",
54
+ )
55
+ _ASCII_CODING_PATTERNS = tuple(
56
+ re.compile(rf"\b{re.escape(keyword)}\b", re.IGNORECASE) for keyword in _ASCII_CODING_KEYWORDS
57
+ )
58
+
59
+
35
60
  def is_coding_task(prompt: str) -> bool:
36
61
  text = prompt.lower()
37
- needles = (
38
- "code",
39
- "fix",
40
- "implement",
41
- "refactor",
42
- "patch",
43
- "test",
44
- "bug",
45
- "코드",
46
- "수정",
47
- "구현",
48
- "리팩터",
49
- "패치",
50
- "테스트",
51
- "버그",
52
- )
53
- return any(needle in text for needle in needles)
62
+ if any(pattern.search(text) for pattern in _ASCII_CODING_PATTERNS):
63
+ return True
64
+ return any(keyword in text for keyword in _KOREAN_CODING_KEYWORDS)
54
65
 
55
66
 
56
67
  __all__ = ["BudgetDecision", "budget_for", "is_coding_task"]
@@ -43,13 +43,17 @@ def build_repo_map(
43
43
  base = Path(root).resolve()
44
44
  if not base.is_dir():
45
45
  return {"ok": False, "error": f"root is not a directory: {base}"}
46
- entries = rust_bridge.scan_repo(base, max_files=max(1, int(max_files)))
46
+ capped_max_files = max(1, int(max_files))
47
+ entries = rust_bridge.scan_repo(base, max_files=capped_max_files)
48
+ total_candidates = rust_bridge.count_scan_candidates(base)
49
+ files_capped = max(0, total_candidates - capped_max_files)
47
50
  entries = _rank_entries(entries)
48
51
 
49
52
  lines: list[str] = []
50
53
  used = 0
51
54
  files_mapped = 0
52
- files_omitted = 0
55
+ files_omitted = files_capped
56
+ budget_exhausted = False
53
57
  outlined_files = 0
54
58
  symbol_total = 0
55
59
  budget = max(200, int(budget_chars))
@@ -57,7 +61,7 @@ def build_repo_map(
57
61
  for entry in entries:
58
62
  rel = str(entry.get("path", ""))
59
63
  total_lines = int(entry.get("total_lines", 0))
60
- if files_omitted: # budget already exhausted: only count the rest
64
+ if budget_exhausted:
61
65
  files_omitted += 1
62
66
  continue
63
67
  block = [f"{rel} ({total_lines}L)"]
@@ -80,6 +84,7 @@ def build_repo_map(
80
84
  block_text = "\n".join(block)
81
85
  if used + len(block_text) + 1 > budget:
82
86
  files_omitted += 1
87
+ budget_exhausted = True
83
88
  continue
84
89
  lines.append(block_text)
85
90
  used += len(block_text) + 1
@@ -90,7 +95,7 @@ def build_repo_map(
90
95
  "root": str(base),
91
96
  "backend": rust_bridge.resolve_backend(),
92
97
  "map": "\n".join(lines),
93
- "files_scanned": len(entries),
98
+ "files_scanned": len(entries) + files_capped,
94
99
  "files_mapped": files_mapped,
95
100
  "files_omitted": files_omitted,
96
101
  "outlined_files": outlined_files,
@@ -120,6 +120,61 @@ def native_module_importable(ctx: DoctorContext) -> tuple[str, str]:
120
120
 
121
121
  # plugin-specific probes (deterministic ones only) - for supercoder we can add if symbols found
122
122
  # for now, handler_exception_coverage is cross-cutting
123
+ _EXPECTED_TOOLS = (
124
+ "supercoder_plan",
125
+ "supercoder_read_window",
126
+ "supercoder_patch",
127
+ "supercoder_cursor_map",
128
+ "supercoder_syntax_gate",
129
+ "supercoder_lint_gate",
130
+ "supercoder_repo_map",
131
+ "supercoder_test_gate",
132
+ "supercoder_brief",
133
+ "supercoder_doctor",
134
+ )
135
+
136
+
137
+ @_register("hermes_contract_tool_registration")
138
+ def hermes_contract_tool_registration(ctx: DoctorContext) -> tuple[str, str]:
139
+ try:
140
+ from cluxion_agentplugin_supercoder.plugin import register
141
+
142
+ class _MockCtx:
143
+ def __init__(self) -> None:
144
+ self.tools: list[tuple[str, str, dict, object, str]] = []
145
+
146
+ def register_tool(
147
+ self,
148
+ *,
149
+ name: str,
150
+ toolset: str,
151
+ schema: dict,
152
+ handler: object,
153
+ emoji: str,
154
+ ) -> None:
155
+ self.tools.append((name, toolset, schema, handler, emoji))
156
+
157
+ mock = _MockCtx()
158
+ register(mock)
159
+ registered = [name for name, *_ in mock.tools]
160
+ missing = [name for name in _EXPECTED_TOOLS if name not in registered]
161
+ if missing:
162
+ return "fail", f"missing tools: {', '.join(missing)}"
163
+ extras = [name for name in registered if name not in _EXPECTED_TOOLS]
164
+ if extras:
165
+ return "fail", f"unexpected tools: {', '.join(extras)}"
166
+ for name, toolset, schema, handler, _emoji in mock.tools:
167
+ if toolset != "supercoder":
168
+ return "fail", f"{name}: toolset={toolset!r}"
169
+ if not isinstance(schema, dict) or not schema.get("name"):
170
+ return "fail", f"{name}: invalid schema"
171
+ if not callable(handler):
172
+ return "fail", f"{name}: handler not callable"
173
+ return "pass", f"{len(_EXPECTED_TOOLS)} tools registered with schemas and handlers"
174
+ except Exception as e:
175
+ return "fail", f"registration error: {e}"
176
+
177
+
123
178
  @_register("handler_exception_coverage")
124
179
  def handler_exception_coverage(ctx: DoctorContext) -> tuple[str, str]:
125
180
  try:
@@ -107,14 +107,7 @@ def _parse_backend_json(raw: str, command: str) -> dict[str, object]:
107
107
  return parsed
108
108
 
109
109
 
110
- def _py_scan(
111
- root: Path,
112
- *,
113
- max_files: int,
114
- extensions: tuple[str, ...],
115
- ) -> dict[str, object]:
116
- from cluxion_agentplugin_supercoder.core.hash_patch import file_hash
117
-
110
+ def _collect_candidates(root: Path, *, extensions: tuple[str, ...]) -> list[str]:
118
111
  candidates: list[str] = []
119
112
  for path in root.rglob("*"):
120
113
  if not path.is_file():
@@ -125,6 +118,27 @@ def _py_scan(
125
118
  continue
126
119
  candidates.append(str(path.relative_to(root)))
127
120
  candidates.sort()
121
+ return candidates
122
+
123
+
124
+ def count_scan_candidates(
125
+ root: Path,
126
+ *,
127
+ extensions: tuple[str, ...] = DEFAULT_EXTENSIONS,
128
+ ) -> int:
129
+ """Return how many files match scan_repo criteria before the max_files cap."""
130
+ return len(_collect_candidates(root, extensions=extensions))
131
+
132
+
133
+ def _py_scan(
134
+ root: Path,
135
+ *,
136
+ max_files: int,
137
+ extensions: tuple[str, ...],
138
+ ) -> dict[str, object]:
139
+ from cluxion_agentplugin_supercoder.core.hash_patch import file_hash
140
+
141
+ candidates = _collect_candidates(root, extensions=extensions)
128
142
  entries: list[dict[str, object]] = []
129
143
  for rel in candidates[:max_files]:
130
144
  try:
@@ -138,7 +152,12 @@ def _py_scan(
138
152
  "total_lines": text.count("\n") + (1 if text else 0),
139
153
  }
140
154
  )
141
- return {"ok": True, "entries": entries, "count": len(entries)}
155
+ return {
156
+ "ok": True,
157
+ "entries": entries,
158
+ "count": len(entries),
159
+ "total_candidates": len(candidates),
160
+ }
142
161
 
143
162
 
144
163
  def _binary() -> str:
@@ -158,6 +177,7 @@ __all__ = [
158
177
  "DEFAULT_MAX_FILES",
159
178
  "INDEX_BACKEND_ENV",
160
179
  "INDEX_BIN_ENV",
180
+ "count_scan_candidates",
161
181
  "index_available",
162
182
  "resolve_backend",
163
183
  "scan_repo",
@@ -223,6 +223,82 @@ def test_path_security_probe_detects_ungated_read(monkeypatch):
223
223
  assert status == "fail"
224
224
 
225
225
 
226
+ def test_healthy_install_ok_and_exit_zero(monkeypatch):
227
+ monkeypatch.setattr(
228
+ "cluxion_agentplugin_supercoder.doctor.probes.shutil.which",
229
+ lambda name: "/usr/bin/hermes" if name == "hermes" else None,
230
+ )
231
+
232
+ def fake_run(cmd, **_kwargs):
233
+ joined = " ".join(cmd)
234
+ if "--version" in joined:
235
+ return subprocess.CompletedProcess(cmd, 0, "Hermes Agent v1.0.0", "")
236
+ if "--help" in joined:
237
+ return subprocess.CompletedProcess(cmd, 0, "-z --oneshot", "")
238
+ if "tools" in joined and "list" in joined:
239
+ return subprocess.CompletedProcess(cmd, 0, "supercoder", "")
240
+ return subprocess.CompletedProcess(cmd, 0, "", "")
241
+
242
+ monkeypatch.setattr(
243
+ "cluxion_agentplugin_supercoder.doctor.framework.subprocess.run",
244
+ fake_run,
245
+ )
246
+ cat = _catalog_path()
247
+ result = run_doctor(
248
+ cwd=Path.cwd(),
249
+ catalog_path=cat,
250
+ probes=PROBES,
251
+ plugin="supercoder",
252
+ version="0.2.4",
253
+ )
254
+ statuses = {c.check_id: c.status for c in result.checks}
255
+ assert statuses["hermes_contract_tool_registration"] == "pass"
256
+ assert result.ok is True
257
+ assert result.summary == "ok"
258
+ payload = json.loads(render_json(result))
259
+ assert payload["ok"] is True
260
+
261
+ from cluxion_agentplugin_supercoder.cli import main
262
+
263
+ assert main(["doctor"]) == 0
264
+
265
+
266
+ def test_genuine_failure_still_degraded_exit_one(monkeypatch):
267
+ monkeypatch.setattr(
268
+ "cluxion_agentplugin_supercoder.doctor.probes.shutil.which",
269
+ lambda name: "/usr/bin/hermes" if name == "hermes" else None,
270
+ )
271
+
272
+ def fake_run(cmd, **_kwargs):
273
+ joined = " ".join(cmd)
274
+ if "--version" in joined:
275
+ return subprocess.CompletedProcess(cmd, 0, "Hermes Agent v1.0.0", "")
276
+ if "--help" in joined:
277
+ return subprocess.CompletedProcess(cmd, 0, "-z --oneshot", "")
278
+ if "tools" in joined and "list" in joined:
279
+ return subprocess.CompletedProcess(cmd, 1, "", "tools unavailable")
280
+ return subprocess.CompletedProcess(cmd, 0, "", "")
281
+
282
+ monkeypatch.setattr(
283
+ "cluxion_agentplugin_supercoder.doctor.framework.subprocess.run",
284
+ fake_run,
285
+ )
286
+ result = run_doctor(
287
+ cwd=Path.cwd(),
288
+ catalog_path=_catalog_path(),
289
+ probes=PROBES,
290
+ plugin="supercoder",
291
+ version="0.2.4",
292
+ )
293
+ statuses = {c.check_id: c.status for c in result.checks}
294
+ assert statuses["toolset_valid"] == "fail"
295
+ assert result.ok is False
296
+
297
+ from cluxion_agentplugin_supercoder.cli import main
298
+
299
+ assert main(["doctor"]) == 1
300
+
301
+
226
302
  def test_hermes_workspace_probe_detects_ungated_read(monkeypatch):
227
303
  from cluxion_agentplugin_supercoder.core.safety import SafetyDecision
228
304
  from cluxion_agentplugin_supercoder.doctor.probes import hermes_context_workspace_root
@@ -158,6 +158,24 @@ def test_concurrent_patches_no_lost_update(tmp_path: Path) -> None:
158
158
  assert new_hash != file_hash(initial)
159
159
 
160
160
 
161
+ def test_concurrent_patches_no_lost_update_stress(tmp_path: Path) -> None:
162
+ """50 iterations of 8 concurrent workers: zero lost updates across all runs."""
163
+ for _ in range(50):
164
+ path = tmp_path / "concurrent_stress.py"
165
+ initial = "\n".join(f"# UNIQUE_PATCH_{i}_START" for i in range(8)) + "\n"
166
+ path.write_text(initial, encoding="utf-8")
167
+
168
+ with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
169
+ futures = [executor.submit(_worker_apply, i, path) for i in range(8)]
170
+ successes = [f.result() for f in concurrent.futures.as_completed(futures)]
171
+
172
+ assert all(successes), "Some patches lost due to race"
173
+ final = path.read_text(encoding="utf-8")
174
+ for i in range(8):
175
+ assert f"# UNIQUE_PATCH_{i}_DONE" in final
176
+ assert f"# UNIQUE_PATCH_{i}_START" not in final
177
+
178
+
161
179
  def test_atomic_write_interruption_leaves_original_intact(tmp_path: Path) -> None:
162
180
  """Simulated mid-write crash leaves ORIGINAL file intact (temp may remain, no truncate)."""
163
181
  path = tmp_path / "atomic_test.txt"
@@ -32,3 +32,16 @@ def test_is_coding_task_korean_and_english() -> None:
32
32
  assert is_coding_task("fix the login bug")
33
33
  assert is_coding_task("이 버그 수정해줘")
34
34
  assert not is_coding_task("오늘 날씨 어때?")
35
+
36
+
37
+ def test_is_coding_task_word_boundaries_avoid_false_positives() -> None:
38
+ assert not is_coding_task("what is the latest news")
39
+ assert not is_coding_task("add a prefix to each line")
40
+ assert not is_coding_task("who won the contest yesterday")
41
+
42
+
43
+ def test_is_coding_task_true_positives_with_word_boundaries() -> None:
44
+ assert is_coding_task("fix the bug in main.py")
45
+ assert is_coding_task("add a unit test for login")
46
+ assert is_coding_task("refactor the auth module")
47
+ assert is_coding_task("debug the timeout issue")
@@ -82,6 +82,17 @@ def test_rust_outline_in_tree_sitter_tiers_fails_open_in_python(backend: str, sa
82
82
  assert ("method", "run") in pairs
83
83
 
84
84
 
85
+ def test_max_files_cap_is_surfaced_honestly(backend: str, tmp_path: Path) -> None:
86
+ for index in range(6):
87
+ (tmp_path / f"mod_{index}.py").write_text(f"def fn_{index}():\n pass\n", encoding="utf-8")
88
+ result = repo_map.build_repo_map(tmp_path, max_files=3, budget_chars=8000)
89
+ assert result["truncated"] is True
90
+ assert result["files_omitted"] > 0
91
+ assert result["files_mapped"] + result["files_omitted"] == result["files_scanned"] == 6
92
+ assert result["files_omitted"] == 3
93
+ assert result["files_mapped"] == 3
94
+
95
+
85
96
  def test_budget_omits_files_honestly(backend: str, sample_repo: Path) -> None:
86
97
  # budget_chars clamps to a 200-char floor, so overflow it with real files.
87
98
  for index in range(8):
@@ -160,7 +160,7 @@ wheels = [
160
160
 
161
161
  [[package]]
162
162
  name = "cluxion-agentplugin-supercoder"
163
- version = "0.2.9"
163
+ version = "0.2.11"
164
164
  source = { editable = "." }
165
165
  dependencies = [
166
166
  { name = "psutil" },