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.
- cluxion_agentplugin_supercoder/__init__.py +5 -0
- cluxion_agentplugin_supercoder/cli.py +27 -0
- cluxion_agentplugin_supercoder/core/cursor.py +93 -0
- cluxion_agentplugin_supercoder/core/hash_patch.py +140 -0
- cluxion_agentplugin_supercoder/core/line_budget.py +56 -0
- cluxion_agentplugin_supercoder/core/queue.py +96 -0
- cluxion_agentplugin_supercoder/core/safety.py +59 -0
- cluxion_agentplugin_supercoder/core/test_gate.py +71 -0
- cluxion_agentplugin_supercoder/plugin.py +91 -0
- cluxion_agentplugin_supercoder/runner.py +111 -0
- cluxion_agentplugin_supercoder/rust_bridge.py +45 -0
- cluxion_agentplugin_supercoder/schemas.py +78 -0
- cluxion_agentplugin_supercoder-0.1.0.dist-info/METADATA +100 -0
- cluxion_agentplugin_supercoder-0.1.0.dist-info/RECORD +16 -0
- cluxion_agentplugin_supercoder-0.1.0.dist-info/WHEEL +4 -0
- cluxion_agentplugin_supercoder-0.1.0.dist-info/entry_points.txt +6 -0
|
@@ -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,,
|