cluxion-agentplugin-supercoder 0.2.0__cp311-abi3-win_amd64.whl

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.
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from collections.abc import Sequence
7
+
8
+ from cluxion_agentplugin_supercoder import __version__
9
+ from cluxion_agentplugin_supercoder.rust_bridge import index_available, resolve_backend
10
+
11
+
12
+ def main(argv: Sequence[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(prog="cluxion-supercoder")
14
+ parser.add_argument("--version", action="version", version=f"cluxion-agentplugin-supercoder {__version__}")
15
+ sub = parser.add_subparsers(dest="command")
16
+ sub.add_parser("check", help="Check plugin and Rust index availability")
17
+ args = parser.parse_args(argv)
18
+ if args.command == "check":
19
+ payload = {
20
+ "plugin": "cluxion-agentplugin-supercoder",
21
+ "version": __version__,
22
+ "rust_index": index_available(),
23
+ "index_backend": resolve_backend(),
24
+ }
25
+ print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
26
+ return 0
27
+ parser.print_help(sys.stderr)
28
+ return 2
29
+
30
+
31
+ if __name__ == "__main__":
32
+ raise SystemExit(main())
@@ -0,0 +1,82 @@
1
+ """Cursor logic — bounded file windows with hash verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from cluxion_agentplugin_supercoder.core.hash_patch import file_hash
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class LineWindow:
13
+ path: str
14
+ start_line: int
15
+ end_line: int
16
+ content: str
17
+ content_hash: str
18
+ file_hash: str
19
+ purpose: str = "read"
20
+
21
+
22
+ def read_window(
23
+ root: Path,
24
+ rel_path: str,
25
+ *,
26
+ start_line: int = 1,
27
+ max_lines: int = 120,
28
+ purpose: str = "read",
29
+ ) -> LineWindow:
30
+ path = (root / rel_path).resolve()
31
+ if not path.exists():
32
+ raise FileNotFoundError(rel_path)
33
+ if not str(path).startswith(str(root.resolve())):
34
+ raise PermissionError("path escapes workspace root")
35
+ text = path.read_text(encoding="utf-8")
36
+ lines = text.splitlines()
37
+ start = max(1, start_line)
38
+ end = min(len(lines), start + max_lines - 1)
39
+ if start > len(lines):
40
+ excerpt = ""
41
+ end = start
42
+ else:
43
+ excerpt = "\n".join(lines[start - 1 : end])
44
+ return LineWindow(
45
+ path=rel_path,
46
+ start_line=start,
47
+ end_line=end,
48
+ content=excerpt,
49
+ content_hash=file_hash(excerpt),
50
+ file_hash=file_hash(text),
51
+ purpose=purpose,
52
+ )
53
+
54
+
55
+ def cursor_map(root: Path, *, paths: list[str] | None = None, max_files: int = 64) -> list[dict[str, object]]:
56
+ if paths is None:
57
+ from cluxion_agentplugin_supercoder.rust_bridge import scan_repo
58
+
59
+ scanned = scan_repo(root, max_files=max_files)
60
+ return [{**entry, "purpose": "index"} for entry in scanned]
61
+ entries: list[dict[str, object]] = []
62
+ for rel in paths[:max_files]:
63
+ path = root / rel
64
+ if not path.is_file():
65
+ continue
66
+ try:
67
+ text = path.read_text(encoding="utf-8")
68
+ except (OSError, UnicodeDecodeError):
69
+ continue
70
+ lines = text.count("\n") + (1 if text else 0)
71
+ entries.append(
72
+ {
73
+ "path": rel,
74
+ "file_hash": file_hash(text),
75
+ "total_lines": lines,
76
+ "purpose": "index",
77
+ }
78
+ )
79
+ return entries
80
+
81
+
82
+ __all__ = ["LineWindow", "cursor_map", "read_window"]
@@ -0,0 +1,150 @@
1
+ """Hash-verified safe patch — ported from cluxion-os _hash_edit_core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass
7
+ from difflib import SequenceMatcher
8
+ from pathlib import Path
9
+
10
+ DEFAULT_FUZZY_THRESHOLD = 0.86
11
+ MAX_CONTEXT_SCAN = 8
12
+ MAX_LINE_DRIFT = 2
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class PatchResult:
17
+ success: bool
18
+ file_path: str
19
+ strategy: str
20
+ message: str
21
+ expected_hash: str
22
+ matched_hash: str | None = None
23
+ similarity: float = 0.0
24
+ replacements: int = 0
25
+
26
+
27
+ def file_hash(content: str) -> str:
28
+ return hashlib.sha256(_normalize_newlines(content).encode("utf-8")).hexdigest()
29
+
30
+
31
+ def hash_block(content: str, context_lines: int) -> str:
32
+ normalized = _normalize_newlines(content)
33
+ material = f"context_lines={context_lines}\0{normalized}"
34
+ return hashlib.sha256(material.encode("utf-8")).hexdigest()
35
+
36
+
37
+ def apply_patch(
38
+ path: Path,
39
+ *,
40
+ old_text: str,
41
+ new_text: str,
42
+ expected_file_hash: str = "",
43
+ fuzzy_threshold: float = DEFAULT_FUZZY_THRESHOLD,
44
+ ) -> PatchResult:
45
+ if not path.exists():
46
+ 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")
69
+
70
+
71
+ def _normalize_newlines(content: str) -> str:
72
+ return content.replace("\r\n", "\n").replace("\r", "\n")
73
+
74
+
75
+ def _normalize_hash(value: str) -> str:
76
+ raw = value.strip().lower()
77
+ if raw.startswith("sha256:"):
78
+ raw = raw.removeprefix("sha256:")
79
+ if len(raw) != 64:
80
+ raise ValueError("hash must be 64-char sha256")
81
+ return raw
82
+
83
+
84
+ def _exact_spans(text: str, needle: str) -> list[tuple[int, int]]:
85
+ spans: list[tuple[int, int]] = []
86
+ offset = 0
87
+ while True:
88
+ start = text.find(needle, offset)
89
+ if start < 0:
90
+ return spans
91
+ spans.append((start, start + len(needle)))
92
+ offset = start + len(needle)
93
+
94
+
95
+ def _candidate_spans(text: str, reference: str, line_drift: int) -> list[tuple[int, int, str]]:
96
+ lines = text.splitlines(keepends=True)
97
+ if not lines:
98
+ return []
99
+ offsets = [0]
100
+ for line in lines:
101
+ offsets.append(offsets[-1] + len(line))
102
+ target = max(1, len(reference.splitlines(keepends=True)))
103
+ lower = max(1, target - line_drift)
104
+ upper = min(len(lines), target + line_drift)
105
+ spans: list[tuple[int, int, str]] = []
106
+ for width in range(lower, upper + 1):
107
+ for start_line in range(0, len(lines) - width + 1):
108
+ start = offsets[start_line]
109
+ end = offsets[start_line + width]
110
+ block = text[start:end]
111
+ spans.append((start, end, block))
112
+ return spans
113
+
114
+
115
+ def _best_fuzzy_span(text: str, reference: str) -> tuple[int, int, str, float, bool] | None:
116
+ best: tuple[int, int, str, float] | None = None
117
+ ambiguous = False
118
+ for start, end, block in _candidate_spans(text, reference, MAX_LINE_DRIFT):
119
+ score = SequenceMatcher(None, block, reference, autojunk=False).ratio()
120
+ if best is None or score > best[3]:
121
+ best = (start, end, block, score)
122
+ ambiguous = False
123
+ elif score >= DEFAULT_FUZZY_THRESHOLD and best and abs(score - best[3]) < 0.015:
124
+ ambiguous = True
125
+ if best is None:
126
+ return None
127
+ return best[0], best[1], best[2], best[3], ambiguous
128
+
129
+
130
+ def _commit(
131
+ path: Path,
132
+ text: str,
133
+ start: int,
134
+ end: int,
135
+ new_content: str,
136
+ strategy: str,
137
+ expected: str,
138
+ matched: str,
139
+ score: float,
140
+ ) -> PatchResult:
141
+ updated = f"{text[:start]}{new_content}{text[end:]}"
142
+ path.write_text(updated, encoding="utf-8")
143
+ return PatchResult(True, str(path), strategy, "patch applied", expected, matched, round(score, 4), 1)
144
+
145
+
146
+ def _failed(path: str, strategy: str, expected: str, message: str, score: float = 0.0) -> PatchResult:
147
+ return PatchResult(False, path, strategy, message, expected, None, round(score, 4), 0)
148
+
149
+
150
+ __all__ = ["PatchResult", "apply_patch", "file_hash", "hash_block"]
@@ -0,0 +1,56 @@
1
+ """Line budget policy — blocks oversized reads and writes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class BudgetDecision:
10
+ allowed: bool
11
+ reason: str
12
+ max_lines: int
13
+ remaining_lines: int
14
+
15
+
16
+ _DEFAULTS = {
17
+ "inspect": 120,
18
+ "patch_context": 100,
19
+ "review": 160,
20
+ "refactor_unit": 250,
21
+ "create_file": 400,
22
+ "test_log": 120,
23
+ }
24
+
25
+
26
+ def budget_for(mode: str, *, requested_lines: int, remaining: int = 10_000) -> BudgetDecision:
27
+ cap = _DEFAULTS.get(mode, 120)
28
+ if requested_lines > cap:
29
+ return BudgetDecision(False, f"line_budget_exceeded:{mode}", cap, remaining)
30
+ if requested_lines > remaining:
31
+ return BudgetDecision(False, "session_line_budget_exhausted", cap, remaining)
32
+ return BudgetDecision(True, "within_budget", cap, remaining - requested_lines)
33
+
34
+
35
+ def is_coding_task(prompt: str) -> bool:
36
+ 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)
54
+
55
+
56
+ __all__ = ["BudgetDecision", "budget_for", "is_coding_task"]
@@ -0,0 +1,111 @@
1
+ """L2 lint gate: advisory lint findings for the file a patch just changed.
2
+
3
+ The engine is ruff — a Rust linter shipped as a wheel dependency of this
4
+ plugin, so the gate works everywhere without asking the host project to
5
+ install anything. It runs on a single file, respects the target project's
6
+ own ruff configuration (config discovery walks up from the file), and is
7
+ suggest-only: findings ride along on the patch result and never block or
8
+ revert a patch. Languages without a wired linter report ``checked: False``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ from functools import lru_cache
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from cluxion_agentplugin_supercoder.core.syntax_gate import language_for_path
23
+
24
+ RUFF_BIN_ENV = "CLUXION_SUPERCODER_RUFF_BIN"
25
+ LINTABLE_LANGUAGES = {"python"}
26
+ MAX_REPORTED_FINDINGS = 20
27
+ _TIMEOUT_SECONDS = 15.0
28
+
29
+
30
+ def ruff_bin() -> str | None:
31
+ """Resolve the ruff executable: env override, venv sibling, then PATH."""
32
+ override = os.environ.get(RUFF_BIN_ENV, "").strip()
33
+ if override:
34
+ return override if Path(override).exists() else None
35
+ return _discover_ruff()
36
+
37
+
38
+ @lru_cache(maxsize=1)
39
+ def _discover_ruff() -> str | None:
40
+ name = "ruff.exe" if os.name == "nt" else "ruff"
41
+ sibling = Path(sys.executable).with_name(name)
42
+ if sibling.exists():
43
+ return str(sibling)
44
+ return shutil.which("ruff")
45
+
46
+
47
+ def check_file(path: str | Path, *, cwd: str | Path | None = None) -> dict[str, Any]:
48
+ """Lint one file and return structured advisory findings."""
49
+ target = Path(path)
50
+ language = language_for_path(target)
51
+ if language not in LINTABLE_LANGUAGES:
52
+ return _unchecked(language or "", "no_linter")
53
+ binary = ruff_bin()
54
+ if binary is None:
55
+ return _unchecked(language, "no_tool")
56
+ command = [binary, "check", "--output-format", "json", "--force-exclude", "--no-cache", str(target)]
57
+ try:
58
+ proc = subprocess.run(
59
+ command,
60
+ cwd=str(cwd) if cwd is not None else None,
61
+ capture_output=True,
62
+ text=True,
63
+ timeout=_TIMEOUT_SECONDS,
64
+ )
65
+ except (OSError, subprocess.TimeoutExpired) as exc:
66
+ return _unchecked(language, f"tool_error:{type(exc).__name__}")
67
+ # ruff exits 0 (clean) or 1 (findings); anything else is a tool failure.
68
+ if proc.returncode not in (0, 1):
69
+ return _unchecked(language, "tool_error:exit")
70
+ try:
71
+ raw = json.loads(proc.stdout or "[]")
72
+ except json.JSONDecodeError:
73
+ return _unchecked(language, "tool_error:output")
74
+ findings = [
75
+ {
76
+ "line": int(item.get("location", {}).get("row", 1)),
77
+ "column": int(item.get("location", {}).get("column", 1)),
78
+ "code": item.get("code") or "",
79
+ "message": str(item.get("message", "")),
80
+ "fixable": item.get("fix") is not None,
81
+ }
82
+ for item in raw
83
+ if isinstance(item, dict)
84
+ ]
85
+ total = len(findings)
86
+ return {
87
+ "ok": True,
88
+ "checked": True,
89
+ "language": language,
90
+ "tool": "ruff",
91
+ "clean": total == 0,
92
+ "findings": findings[:MAX_REPORTED_FINDINGS],
93
+ "finding_count": total,
94
+ "truncated": total > MAX_REPORTED_FINDINGS,
95
+ }
96
+
97
+
98
+ def _unchecked(language: str, reason: str) -> dict[str, Any]:
99
+ return {
100
+ "ok": True,
101
+ "checked": False,
102
+ "language": language,
103
+ "reason": reason,
104
+ "clean": True,
105
+ "findings": [],
106
+ "finding_count": 0,
107
+ "truncated": False,
108
+ }
109
+
110
+
111
+ __all__ = ["LINTABLE_LANGUAGES", "MAX_REPORTED_FINDINGS", "RUFF_BIN_ENV", "check_file", "ruff_bin"]
@@ -0,0 +1,96 @@
1
+ """Coding work unit queue — deterministic, no model calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+
8
+
9
+ class WorkStatus(StrEnum):
10
+ PENDING = "pending"
11
+ RUNNING = "running"
12
+ BLOCKED = "blocked"
13
+ DEFERRED = "deferred"
14
+ COMPLETE = "complete"
15
+ FAILED = "failed"
16
+
17
+
18
+ @dataclass
19
+ class WorkUnit:
20
+ id: str
21
+ goal: str
22
+ priority: int = 2
23
+ allowed_paths: tuple[str, ...] = ()
24
+ line_budget: int = 250
25
+ status: WorkStatus = WorkStatus.PENDING
26
+ expected_evidence: tuple[str, ...] = ()
27
+ dependencies: tuple[str, ...] = ()
28
+
29
+
30
+ @dataclass
31
+ class TaskQueue:
32
+ task_id: str
33
+ units: list[WorkUnit] = field(default_factory=list)
34
+
35
+ def enqueue(self, unit: WorkUnit) -> None:
36
+ self.units.append(unit)
37
+
38
+ def next_unit(self) -> WorkUnit | None:
39
+ completed = {unit.id for unit in self.units if unit.status == WorkStatus.COMPLETE}
40
+ for unit in sorted(self.units, key=lambda item: (item.priority, item.id)):
41
+ if unit.status != WorkStatus.PENDING:
42
+ continue
43
+ if all(dep in completed for dep in unit.dependencies):
44
+ unit.status = WorkStatus.RUNNING
45
+ return unit
46
+ return None
47
+
48
+ def record(self, unit_id: str, *, success: bool, evidence: tuple[str, ...] = ()) -> dict[str, object]:
49
+ for unit in self.units:
50
+ if unit.id != unit_id:
51
+ continue
52
+ unit.status = WorkStatus.COMPLETE if success else WorkStatus.FAILED
53
+ unit.expected_evidence = evidence or unit.expected_evidence
54
+ return {
55
+ "task_id": self.task_id,
56
+ "unit_id": unit_id,
57
+ "status": unit.status.value,
58
+ "remaining": sum(1 for item in self.units if item.status == WorkStatus.PENDING),
59
+ }
60
+ raise KeyError(unit_id)
61
+
62
+
63
+ def plan_coding_task(task_id: str, prompt: str) -> TaskQueue:
64
+ queue = TaskQueue(task_id=task_id)
65
+ queue.enqueue(WorkUnit("map", "Map repo and identify target files", priority=0, expected_evidence=("cursor_map",)))
66
+ queue.enqueue(
67
+ WorkUnit(
68
+ "edit",
69
+ f"Apply focused changes for: {prompt[:240]}",
70
+ priority=1,
71
+ dependencies=("map",),
72
+ expected_evidence=("files_changed",),
73
+ )
74
+ )
75
+ queue.enqueue(
76
+ WorkUnit(
77
+ "verify",
78
+ "Run targeted tests or lint for changed files",
79
+ priority=2,
80
+ dependencies=("edit",),
81
+ expected_evidence=("tests_run",),
82
+ )
83
+ )
84
+ queue.enqueue(
85
+ WorkUnit(
86
+ "brief",
87
+ "Summarize changes, verification, and remaining risks",
88
+ priority=3,
89
+ dependencies=("verify",),
90
+ expected_evidence=("brief",),
91
+ )
92
+ )
93
+ return queue
94
+
95
+
96
+ __all__ = ["TaskQueue", "WorkStatus", "WorkUnit", "plan_coding_task"]