cluxion-agentplugin-supercoder 0.1.0__py3-none-any.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.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,27 @@
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
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 = {"plugin": "cluxion-agentplugin-supercoder", "version": __version__, "rust_index": index_available()}
20
+ print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
21
+ return 0
22
+ parser.print_help(sys.stderr)
23
+ return 2
24
+
25
+
26
+ if __name__ == "__main__":
27
+ raise SystemExit(main())
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ """Cursor logic — bounded file windows with hash verification."""
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
+ entries: list[dict[str, object]] = []
57
+ candidates = paths or _default_scan_paths(root)
58
+ for rel in candidates[:max_files]:
59
+ path = root / rel
60
+ if not path.is_file():
61
+ continue
62
+ try:
63
+ text = path.read_text(encoding="utf-8")
64
+ except (OSError, UnicodeDecodeError):
65
+ continue
66
+ lines = text.count("\n") + (1 if text else 0)
67
+ entries.append(
68
+ {
69
+ "path": rel,
70
+ "file_hash": file_hash(text),
71
+ "total_lines": lines,
72
+ "purpose": "index",
73
+ }
74
+ )
75
+ return entries
76
+
77
+
78
+ def _default_scan_paths(root: Path) -> list[str]:
79
+ found: list[str] = []
80
+ for path in root.rglob("*"):
81
+ if len(found) >= 256:
82
+ break
83
+ if not path.is_file():
84
+ continue
85
+ rel = str(path.relative_to(root))
86
+ if any(part in {".git", "node_modules", ".venv", "dist", "target"} for part in path.parts):
87
+ continue
88
+ if path.suffix in {".py", ".rs", ".ts", ".tsx", ".js", ".go", ".md", ".toml", ".yaml", ".yml"}:
89
+ found.append(rel)
90
+ return found
91
+
92
+
93
+ __all__ = ["LineWindow", "cursor_map", "read_window"]
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ """Hash-verified safe patch — ported from cluxion-os _hash_edit_core."""
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(path, text, fuzzy[0], fuzzy[1], new_text, "fuzzy", expected_file_hash or current_hash, current_hash, fuzzy[3])
58
+ return _failed(str(path), "no_match", expected_file_hash or current_hash, "patch target not found")
59
+
60
+
61
+ def _normalize_newlines(content: str) -> str:
62
+ return content.replace("\r\n", "\n").replace("\r", "\n")
63
+
64
+
65
+ def _normalize_hash(value: str) -> str:
66
+ raw = value.strip().lower()
67
+ if raw.startswith("sha256:"):
68
+ raw = raw.removeprefix("sha256:")
69
+ if len(raw) != 64:
70
+ raise ValueError("hash must be 64-char sha256")
71
+ return raw
72
+
73
+
74
+ def _exact_spans(text: str, needle: str) -> list[tuple[int, int]]:
75
+ spans: list[tuple[int, int]] = []
76
+ offset = 0
77
+ while True:
78
+ start = text.find(needle, offset)
79
+ if start < 0:
80
+ return spans
81
+ spans.append((start, start + len(needle)))
82
+ offset = start + len(needle)
83
+
84
+
85
+ def _candidate_spans(text: str, reference: str, line_drift: int) -> list[tuple[int, int, str]]:
86
+ lines = text.splitlines(keepends=True)
87
+ if not lines:
88
+ return []
89
+ offsets = [0]
90
+ for line in lines:
91
+ offsets.append(offsets[-1] + len(line))
92
+ target = max(1, len(reference.splitlines(keepends=True)))
93
+ lower = max(1, target - line_drift)
94
+ upper = min(len(lines), target + line_drift)
95
+ spans: list[tuple[int, int, str]] = []
96
+ for width in range(lower, upper + 1):
97
+ for start_line in range(0, len(lines) - width + 1):
98
+ start = offsets[start_line]
99
+ end = offsets[start_line + width]
100
+ block = text[start:end]
101
+ spans.append((start, end, block))
102
+ return spans
103
+
104
+
105
+ def _best_fuzzy_span(text: str, reference: str) -> tuple[int, int, str, float, bool] | None:
106
+ best: tuple[int, int, str, float] | None = None
107
+ ambiguous = False
108
+ for start, end, block in _candidate_spans(text, reference, MAX_LINE_DRIFT):
109
+ score = SequenceMatcher(None, block, reference, autojunk=False).ratio()
110
+ if best is None or score > best[3]:
111
+ best = (start, end, block, score)
112
+ ambiguous = False
113
+ elif score >= DEFAULT_FUZZY_THRESHOLD and best and abs(score - best[3]) < 0.015:
114
+ ambiguous = True
115
+ if best is None:
116
+ return None
117
+ return best[0], best[1], best[2], best[3], ambiguous
118
+
119
+
120
+ def _commit(
121
+ path: Path,
122
+ text: str,
123
+ start: int,
124
+ end: int,
125
+ new_content: str,
126
+ strategy: str,
127
+ expected: str,
128
+ matched: str,
129
+ score: float,
130
+ ) -> PatchResult:
131
+ updated = f"{text[:start]}{new_content}{text[end:]}"
132
+ path.write_text(updated, encoding="utf-8")
133
+ return PatchResult(True, str(path), strategy, "patch applied", expected, matched, round(score, 4), 1)
134
+
135
+
136
+ def _failed(path: str, strategy: str, expected: str, message: str, score: float = 0.0) -> PatchResult:
137
+ return PatchResult(False, path, strategy, message, expected, None, round(score, 4), 0)
138
+
139
+
140
+ __all__ = ["PatchResult", "apply_patch", "file_hash", "hash_block"]
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ """Line budget policy — blocks oversized reads and writes."""
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,96 @@
1
+ from __future__ import annotations
2
+
3
+ """Coding work unit queue — deterministic, no model calls."""
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"]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ """Fail-closed safety gates for tool calls."""
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class SafetyDecision:
11
+ decision: str
12
+ reason: str
13
+
14
+
15
+ _DESTRUCTIVE = (
16
+ "rm -rf",
17
+ "git reset --hard",
18
+ "git push --force",
19
+ "drop table",
20
+ "truncate table",
21
+ "kubectl delete",
22
+ )
23
+ _SECRET_PARTS = (".env", "id_rsa", "credentials", "secrets")
24
+
25
+
26
+ def pre_tool_gate(
27
+ tool_name: str,
28
+ args: dict[str, object],
29
+ *,
30
+ workspace: Path,
31
+ stale_cursor: bool = False,
32
+ ) -> SafetyDecision:
33
+ if stale_cursor:
34
+ return SafetyDecision("block", "stale cursor: file changed after read_window")
35
+ command = str(args.get("command", ""))
36
+ if any(token in command.lower() for token in _DESTRUCTIVE):
37
+ return SafetyDecision("block", "destructive command requires explicit approval")
38
+ for key in ("path", "file_path", "target"):
39
+ value = args.get(key)
40
+ if isinstance(value, str) and value:
41
+ decision = _path_gate(workspace, value)
42
+ if decision.decision == "block":
43
+ return decision
44
+ if tool_name in {"write_file", "patch"} and int(args.get("line_count", 0) or 0) > 400:
45
+ return SafetyDecision("block", "write exceeds 400-line soft cap")
46
+ return SafetyDecision("allow", "passed safety gate")
47
+
48
+
49
+ def _path_gate(workspace: Path, rel_or_abs: str) -> SafetyDecision:
50
+ candidate = Path(rel_or_abs)
51
+ resolved = (workspace / candidate).resolve() if not candidate.is_absolute() else candidate.resolve()
52
+ if not str(resolved).startswith(str(workspace.resolve())):
53
+ return SafetyDecision("block", "workspace escape blocked")
54
+ if any(part in _SECRET_PARTS for part in resolved.parts):
55
+ return SafetyDecision("block", "secret file access blocked")
56
+ return SafetyDecision("allow", "path ok")
57
+
58
+
59
+ __all__ = ["SafetyDecision", "pre_tool_gate"]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def _resolve_test_targets(root: Path, files_changed: list[str]) -> list[str]:
7
+ targets: list[str] = []
8
+ for raw in files_changed:
9
+ rel = raw.strip()
10
+ if not rel:
11
+ continue
12
+ path = Path(rel)
13
+ if path.parts and path.parts[0] == "tests" and path.name.startswith("test_") and path.suffix == ".py":
14
+ candidate = root / path
15
+ if candidate.exists():
16
+ targets.append(str(path))
17
+ continue
18
+ if "src" not in path.parts:
19
+ continue
20
+ stem = path.stem
21
+ module = path.parent.name
22
+ for name in (f"test_{stem}.py", f"test_{module}.py"):
23
+ candidate = root / "tests" / name
24
+ if candidate.exists():
25
+ targets.append(str(candidate.relative_to(root)))
26
+ break
27
+ return list(dict.fromkeys(targets))
28
+
29
+
30
+ def suggest_test_commands(
31
+ files_changed: list[str] | None = None,
32
+ *,
33
+ command: str | None = None,
34
+ cwd: Path | None = None,
35
+ ) -> dict[str, object]:
36
+ root = (cwd or Path.cwd()).expanduser().resolve()
37
+ changed = [str(item) for item in (files_changed or []) if str(item).strip()]
38
+ targets = _resolve_test_targets(root, changed) if changed else []
39
+ explicit = (command or "").strip()
40
+ if explicit and explicit != "pytest -q":
41
+ primary = explicit
42
+ source = "explicit_command"
43
+ elif targets:
44
+ primary = "pytest -q " + " ".join(targets)
45
+ source = "mapped_from_files_changed"
46
+ else:
47
+ primary = explicit or "pytest -q"
48
+ source = "default"
49
+ return {
50
+ "ok": True,
51
+ "mode": "suggest_or_run",
52
+ "command": primary,
53
+ "targets": targets,
54
+ "files_changed": changed,
55
+ "source": source,
56
+ "alternatives": _alternatives(targets),
57
+ "note": (
58
+ "Run through host terminal tool; do not claim pass unless command succeeded. "
59
+ "Record stdout/stderr in supercoder_brief.tests_run."
60
+ ),
61
+ }
62
+
63
+
64
+ def _alternatives(targets: list[str]) -> list[str]:
65
+ if not targets:
66
+ return ["pytest -q", "python -m pytest -q"]
67
+ joined = " ".join(targets)
68
+ return [f"pytest -q {joined}", f"python -m pytest -q {joined}"]
69
+
70
+
71
+ __all__ = ["suggest_test_commands"]
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+
6
+ from cluxion_agentplugin_supercoder import runner
7
+ from cluxion_agentplugin_supercoder.core.test_gate import suggest_test_commands
8
+ from cluxion_agentplugin_supercoder.schemas import (
9
+ BRIEF_SCHEMA,
10
+ CURSOR_MAP_SCHEMA,
11
+ PATCH_SCHEMA,
12
+ PLAN_SCHEMA,
13
+ READ_WINDOW_SCHEMA,
14
+ TEST_GATE_SCHEMA,
15
+ )
16
+
17
+
18
+ def register(ctx: object) -> None:
19
+ ctx.register_tool(name="supercoder_plan", toolset="supercoder", schema=PLAN_SCHEMA, handler=_wrap(runner.plan), emoji="🧩")
20
+ ctx.register_tool(
21
+ name="supercoder_read_window",
22
+ toolset="supercoder",
23
+ schema=READ_WINDOW_SCHEMA,
24
+ handler=_wrap(runner.read_window_tool),
25
+ emoji="📖",
26
+ )
27
+ ctx.register_tool(
28
+ name="supercoder_patch",
29
+ toolset="supercoder",
30
+ schema=PATCH_SCHEMA,
31
+ handler=_wrap(runner.patch_tool),
32
+ emoji="🩹",
33
+ )
34
+ ctx.register_tool(
35
+ name="supercoder_cursor_map",
36
+ toolset="supercoder",
37
+ schema=CURSOR_MAP_SCHEMA,
38
+ handler=_wrap(runner.cursor_map_tool),
39
+ emoji="🗺️",
40
+ )
41
+ ctx.register_tool(
42
+ name="supercoder_test_gate",
43
+ toolset="supercoder",
44
+ schema=TEST_GATE_SCHEMA,
45
+ handler=_handle_test_gate,
46
+ emoji="🧪",
47
+ )
48
+ ctx.register_tool(name="supercoder_brief", toolset="supercoder", schema=BRIEF_SCHEMA, handler=_handle_brief, emoji="📋")
49
+
50
+
51
+ def _wrap(callback: Callable[[dict[str, object]], runner.ToolResult]) -> Callable[[dict[str, object]], str]:
52
+ def handler(args: dict[str, object], **_: object) -> str:
53
+ try:
54
+ return callback(args).to_json()
55
+ except (ValueError, FileNotFoundError, PermissionError) as exc:
56
+ return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, sort_keys=True)
57
+
58
+ return handler
59
+
60
+
61
+ def _handle_test_gate(args: dict[str, object], **_: object) -> str:
62
+ from pathlib import Path
63
+
64
+ raw_files = args.get("files_changed", [])
65
+ files_changed = [str(item) for item in raw_files] if isinstance(raw_files, list) else []
66
+ cwd_raw = str(args.get("cwd", ".")).strip() or "."
67
+ payload = suggest_test_commands(
68
+ files_changed,
69
+ command=str(args.get("command", "")).strip() or None,
70
+ cwd=Path(cwd_raw).expanduser().resolve(),
71
+ )
72
+ return json.dumps(payload, ensure_ascii=False, sort_keys=True)
73
+
74
+
75
+ def _handle_brief(args: dict[str, object], **_: object) -> str:
76
+ return json.dumps(
77
+ {
78
+ "ok": True,
79
+ "brief": {
80
+ "files_changed": args.get("files_changed", []),
81
+ "tests_run": args.get("tests_run", []),
82
+ "verification_status": args.get("verification_status", "unknown_after_check"),
83
+ "remaining_risks": args.get("remaining_risks", []),
84
+ },
85
+ },
86
+ ensure_ascii=False,
87
+ sort_keys=True,
88
+ )
89
+
90
+
91
+ __all__ = ["register"]
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from cluxion_agentplugin_supercoder.core.cursor import cursor_map, read_window
9
+ from cluxion_agentplugin_supercoder.core.hash_patch import apply_patch
10
+ from cluxion_agentplugin_supercoder.core.line_budget import budget_for, is_coding_task
11
+ from cluxion_agentplugin_supercoder.core.queue import plan_coding_task
12
+ from cluxion_agentplugin_supercoder.core.safety import pre_tool_gate
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ToolResult:
17
+ ok: bool
18
+ payload: dict[str, object]
19
+
20
+ def to_json(self) -> str:
21
+ return json.dumps({"ok": self.ok, **self.payload}, ensure_ascii=False, sort_keys=True)
22
+
23
+
24
+ def _workspace(payload: Mapping[str, object]) -> Path:
25
+ cwd = str(payload.get("cwd", ".")).strip() or "."
26
+ return Path(cwd).expanduser().resolve()
27
+
28
+
29
+ def plan(payload: Mapping[str, object]) -> ToolResult:
30
+ prompt = str(payload.get("prompt", "")).strip()
31
+ if not prompt:
32
+ raise ValueError("prompt is required")
33
+ if not is_coding_task(prompt):
34
+ return ToolResult(True, {"mode": "bypass", "reason": "not_a_coding_task"})
35
+ task_id = str(payload.get("task_id", "task-default"))
36
+ queue = plan_coding_task(task_id, prompt)
37
+ return ToolResult(
38
+ True,
39
+ {
40
+ "mode": "coding_queue",
41
+ "task_id": task_id,
42
+ "units": [
43
+ {
44
+ "id": unit.id,
45
+ "goal": unit.goal,
46
+ "priority": unit.priority,
47
+ "status": unit.status.value,
48
+ "dependencies": list(unit.dependencies),
49
+ }
50
+ for unit in queue.units
51
+ ],
52
+ },
53
+ )
54
+
55
+
56
+ def read_window_tool(payload: Mapping[str, object]) -> ToolResult:
57
+ root = _workspace(payload)
58
+ rel = str(payload.get("path", "")).strip()
59
+ start = int(payload.get("start_line", 1))
60
+ max_lines = int(payload.get("max_lines", 120))
61
+ decision = budget_for("inspect", requested_lines=max_lines)
62
+ if not decision.allowed:
63
+ return ToolResult(False, {"error": decision.reason, "max_lines": decision.max_lines})
64
+ window = read_window(root, rel, start_line=start, max_lines=max_lines, purpose=str(payload.get("purpose", "read")))
65
+ return ToolResult(
66
+ True,
67
+ {
68
+ "path": window.path,
69
+ "start_line": window.start_line,
70
+ "end_line": window.end_line,
71
+ "content": window.content,
72
+ "content_hash": window.content_hash,
73
+ "file_hash": window.file_hash,
74
+ },
75
+ )
76
+
77
+
78
+ def patch_tool(payload: Mapping[str, object]) -> ToolResult:
79
+ root = _workspace(payload)
80
+ rel = str(payload.get("path", "")).strip()
81
+ gate = pre_tool_gate("patch", payload, workspace=root, stale_cursor=bool(payload.get("stale_cursor", False)))
82
+ if gate.decision == "block":
83
+ return ToolResult(False, {"error": gate.reason})
84
+ result = apply_patch(
85
+ root / rel,
86
+ old_text=str(payload.get("old_text", "")),
87
+ new_text=str(payload.get("new_text", "")),
88
+ expected_file_hash=str(payload.get("expected_file_hash", "")),
89
+ )
90
+ return ToolResult(
91
+ result.success,
92
+ {
93
+ "file_path": result.file_path,
94
+ "strategy": result.strategy,
95
+ "message": result.message,
96
+ "expected_hash": result.expected_hash,
97
+ "matched_hash": result.matched_hash,
98
+ "similarity": result.similarity,
99
+ },
100
+ )
101
+
102
+
103
+ def cursor_map_tool(payload: Mapping[str, object]) -> ToolResult:
104
+ root = _workspace(payload)
105
+ paths = payload.get("paths")
106
+ rel_paths = [str(item) for item in paths] if isinstance(paths, list) else None
107
+ entries = cursor_map(root, paths=rel_paths)
108
+ return ToolResult(True, {"entries": entries, "count": len(entries)})
109
+
110
+
111
+ __all__ = ["ToolResult", "cursor_map_tool", "patch_tool", "plan", "read_window_tool"]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+
10
+ def index_available() -> bool:
11
+ return shutil.which(_binary()) is not None
12
+
13
+
14
+ def fast_file_hash(path: Path) -> str:
15
+ binary = _binary()
16
+ if shutil.which(binary) is None:
17
+ from cluxion_agentplugin_supercoder.core.hash_patch import file_hash
18
+
19
+ return file_hash(path.read_text(encoding="utf-8"))
20
+ completed = subprocess.run(
21
+ [binary, "hash"],
22
+ input=json.dumps({"path": str(path)}),
23
+ text=True,
24
+ capture_output=True,
25
+ check=False,
26
+ )
27
+ if completed.returncode != 0:
28
+ from cluxion_agentplugin_supercoder.core.hash_patch import file_hash
29
+
30
+ return file_hash(path.read_text(encoding="utf-8"))
31
+ payload = json.loads(completed.stdout)
32
+ return str(payload.get("hash", ""))
33
+
34
+
35
+ def _binary() -> str:
36
+ configured = os.environ.get("CLUXION_SUPERCODER_INDEX_BIN", "").strip()
37
+ if configured:
38
+ return configured
39
+ local = Path(__file__).resolve().parents[2] / "rust" / "supercoder_index" / "target" / "release" / "supercoder-index"
40
+ if local.exists():
41
+ return str(local)
42
+ return "supercoder-index"
43
+
44
+
45
+ __all__ = ["fast_file_hash", "index_available"]
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ PLAN_SCHEMA = {
4
+ "name": "supercoder_plan",
5
+ "description": "Decompose a coding task into WorkUnit queue.",
6
+ "parameters": {
7
+ "type": "object",
8
+ "properties": {
9
+ "prompt": {"type": "string"},
10
+ "task_id": {"type": "string"},
11
+ "cwd": {"type": "string"},
12
+ },
13
+ "required": ["prompt"],
14
+ },
15
+ }
16
+
17
+ READ_WINDOW_SCHEMA = {
18
+ "name": "supercoder_read_window",
19
+ "description": "Read a bounded line window with hashes.",
20
+ "parameters": {
21
+ "type": "object",
22
+ "properties": {
23
+ "path": {"type": "string"},
24
+ "start_line": {"type": "integer", "default": 1},
25
+ "max_lines": {"type": "integer", "default": 120},
26
+ "cwd": {"type": "string"},
27
+ },
28
+ "required": ["path"],
29
+ },
30
+ }
31
+
32
+ PATCH_SCHEMA = {
33
+ "name": "supercoder_patch",
34
+ "description": "Apply hash-verified patch with stale cursor protection.",
35
+ "parameters": {
36
+ "type": "object",
37
+ "properties": {
38
+ "path": {"type": "string"},
39
+ "old_text": {"type": "string"},
40
+ "new_text": {"type": "string"},
41
+ "expected_file_hash": {"type": "string"},
42
+ "cwd": {"type": "string"},
43
+ },
44
+ "required": ["path", "old_text", "new_text"],
45
+ },
46
+ }
47
+
48
+ CURSOR_MAP_SCHEMA = {
49
+ "name": "supercoder_cursor_map",
50
+ "description": "Build repo/file cursor index.",
51
+ "parameters": {
52
+ "type": "object",
53
+ "properties": {"cwd": {"type": "string"}, "paths": {"type": "array", "items": {"type": "string"}}},
54
+ },
55
+ }
56
+
57
+ TEST_GATE_SCHEMA = {
58
+ "name": "supercoder_test_gate",
59
+ "description": "Suggest targeted pytest commands from changed files; host terminal runs them.",
60
+ "parameters": {
61
+ "type": "object",
62
+ "properties": {
63
+ "files_changed": {
64
+ "type": "array",
65
+ "items": {"type": "string"},
66
+ "description": "Paths edited in this task (maps src/* to tests/test_*.py when present).",
67
+ },
68
+ "command": {"type": "string", "description": "Override suggested command (optional)."},
69
+ "cwd": {"type": "string", "description": "Workspace root for test discovery."},
70
+ },
71
+ },
72
+ }
73
+
74
+ BRIEF_SCHEMA = {
75
+ "name": "supercoder_brief",
76
+ "description": "Summarize changes, verification, and remaining risks.",
77
+ "parameters": {"type": "object", "properties": {}},
78
+ }
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: cluxion-agentplugin-supercoder
3
+ Version: 0.1.0
4
+ Summary: Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates.
5
+ Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-supercoder
6
+ Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-supercoder
7
+ Project-URL: Issues, https://github.com/cluxion/cluxion-Agentplugin-supercoder/issues
8
+ Author-email: cluxion <algocean1204@users.noreply.github.com>
9
+ License-Expression: Apache-2.0
10
+ Keywords: claude-code,cluxion,codex,coding,hermes-agent,plugin
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: psutil>=5.9
13
+ Requires-Dist: pyyaml>=6.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: build>=1.2; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: ruff>=0.8; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # cluxion-Agentplugin-supercoder
21
+
22
+ 범용 에이전트 **코딩 하네스 플러그인** — **Hermes, Claude Code, Codex, Grok Build**에서 동일 core로 동작합니다.
23
+
24
+ **Repository:** https://github.com/cluxion/cluxion-Agentplugin-supercoder
25
+
26
+ ## 한 줄 요약
27
+
28
+ 코딩 시 **큰 파일 추측·stale patch**를 막습니다. **연결된 AI**가 `supercoder_*` 도구로 bounded read·hash 검증 patch·evidence brief를 수행합니다.
29
+
30
+ ## 범용 에이전트 + Rust-First
31
+
32
+ | 계층 | 구현 |
33
+ |------|------|
34
+ | **Rust** (`supercoder-index`) | file hash, repo scan |
35
+ | **Python** (`core/`, `runner`) | cursor, patch, safety, plugin |
36
+ | **Agent adapter** | Hermes plugin + `supercoder` toolset |
37
+
38
+ patch 검증·인덱싱은 Rust, orchestration은 Python 래퍼입니다.
39
+
40
+ ## 이 플러그인의 역할
41
+
42
+ - **Cursor logic** — line window, file/content hash
43
+ - **Safe patch** — hash 검증, stale 차단
44
+ - **Line budget** — 과도한 read/write 차단
45
+ - **WorkUnit queue** — map → edit → verify → brief
46
+ - **Safety gate** — workspace escape·destructive command 차단
47
+
48
+ **모델·OAuth는 host 소유.** Supercoder는 plan·검증·evidence 계약만 제공합니다.
49
+
50
+ ## 연결된 AI가 하는 일
51
+
52
+ ```
53
+ 코딩 요청 → supercoder_plan
54
+ → cursor_map / read_window
55
+ → supercoder_patch (hash 필수)
56
+ → test_gate (host terminal 실행)
57
+ → supercoder_brief (evidence 필수)
58
+ ```
59
+
60
+ 비코딩 질문은 `bypass`로 오버헤드 최소화.
61
+
62
+ ## 빠른 시작
63
+
64
+ ```bash
65
+ pip install cluxion-agentplugin-supercoder
66
+ cluxion-supercoder check
67
+ hermes plugins enable cluxion-agentplugin-supercoder
68
+ ```
69
+
70
+ ## 도구 (`supercoder` toolset)
71
+
72
+ | Tool | 설명 |
73
+ |------|------|
74
+ | `supercoder_plan` | WorkUnit 큐 |
75
+ | `supercoder_read_window` | line-bounded read |
76
+ | `supercoder_patch` | hash 검증 patch |
77
+ | `supercoder_cursor_map` | repo index |
78
+ | `supercoder_test_gate` | 테스트 제안 |
79
+ | `supercoder_brief` | evidence 요약 |
80
+
81
+ ## Adapters
82
+
83
+ - `adapters/hermes/` — Hermes enable 가이드
84
+ - `adapters/claude/skills/supercoder/` — Claude skill
85
+ - `adapters/codex/config-snippet.toml` — Codex 참고
86
+
87
+ ## 문서
88
+
89
+ - [Docs/README.md](Docs/README.md) — **처음 읽는 분** + 목차
90
+ - [Docs/architecture.md](Docs/architecture.md)
91
+ - [Docs/design.md](Docs/design.md)
92
+ - [Docs/installation.md](Docs/installation.md)
93
+ - [Docs/tools.md](Docs/tools.md)
94
+ - [Docs/agent-surfaces.md](Docs/agent-surfaces.md)
95
+ - [Docs/capabilities.md](Docs/capabilities.md)
96
+ - [Docs/rust-architecture.md](Docs/rust-architecture.md)
97
+
98
+ ## License
99
+
100
+ Apache-2.0
@@ -0,0 +1,16 @@
1
+ cluxion_agentplugin_supercoder/__init__.py,sha256=KAKeuFanmN4-M5U_soTZl7ymLes67dicCUq-yGdUBpQ,84
2
+ cluxion_agentplugin_supercoder/cli.py,sha256=bI4ABt3k4EVGssKAz2XsgeWy5SKMliKLWOwDFe_kU-4,973
3
+ cluxion_agentplugin_supercoder/plugin.py,sha256=PlacpR3K_B-3p807kFNijpyQRyvSb32IWhSJMqGYpA0,2962
4
+ cluxion_agentplugin_supercoder/runner.py,sha256=ugS38uG9onQ2mRbGehqbLs68uHmUvPt6MTxpj0bTvIw,3964
5
+ cluxion_agentplugin_supercoder/rust_bridge.py,sha256=1Kx_SULF3k-uTrusccoVfMzg9dlIrRz9NtCQD7ZjRgc,1262
6
+ cluxion_agentplugin_supercoder/schemas.py,sha256=pb_Pymw0os8sSb8V98FgXA9O6WoDTduAjLF5IO1-Eng,2463
7
+ cluxion_agentplugin_supercoder/core/cursor.py,sha256=OfTHnDBjRPyeSFbhqDCBdz20uifs3vA4iPeGmLc_GnI,2622
8
+ cluxion_agentplugin_supercoder/core/hash_patch.py,sha256=_0wT2jNqRVs51BA4zKK9QcalCszJsBuzqVCJ1CvbSvo,4757
9
+ cluxion_agentplugin_supercoder/core/line_budget.py,sha256=zI_pFTxNuFADjmRQJtmKVPMH2iDphIK1SICuL1z7E8Q,1325
10
+ cluxion_agentplugin_supercoder/core/queue.py,sha256=HqlM9r__x0Lv7-CAedx-1pLsojDNlLcokrBBDhV1rts,2930
11
+ cluxion_agentplugin_supercoder/core/safety.py,sha256=72YQNEBhBbqsqrQM_FXf-ecSjNyIQgYBRwwOCX1sOxI,1922
12
+ cluxion_agentplugin_supercoder/core/test_gate.py,sha256=xyDfkYadSWmR4PZEX5b0ePhYpjfnc_4rufiN9Cc4sRw,2317
13
+ cluxion_agentplugin_supercoder-0.1.0.dist-info/METADATA,sha256=_oFcnDxcj5i9_zFv7rJTy4hoAyyjE8kiXMbPK8q11_0,3424
14
+ cluxion_agentplugin_supercoder-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
+ cluxion_agentplugin_supercoder-0.1.0.dist-info/entry_points.txt,sha256=qQpohjNavNMcFwAQasvyIrghVWKifJJ5T3iV_SP04io,232
16
+ cluxion_agentplugin_supercoder-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ cluxion-supercoder = cluxion_agentplugin_supercoder.cli:main
3
+
4
+ [hermes_agent.plugins]
5
+ cluxion-agentplugin-supercoder = cluxion_agentplugin_supercoder.plugin
6
+ hermes-supercoder = cluxion_agentplugin_supercoder.plugin