sin-code-bundle 0.9.2__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.
- sin_code_bundle/__init__.py +6 -0
- sin_code_bundle/agents_md.py +245 -0
- sin_code_bundle/ast_edit.py +323 -0
- sin_code_bundle/bench.py +506 -0
- sin_code_bundle/budget.py +51 -0
- sin_code_bundle/cache.py +131 -0
- sin_code_bundle/checkpoint.py +230 -0
- sin_code_bundle/cli.py +1943 -0
- sin_code_bundle/codocs.py +328 -0
- sin_code_bundle/dap_bridge.py +135 -0
- sin_code_bundle/data/codocs/SKILL.md +280 -0
- sin_code_bundle/gitnexus.py +368 -0
- sin_code_bundle/hashline.py +216 -0
- sin_code_bundle/hooks.py +249 -0
- sin_code_bundle/immortal_commit.py +288 -0
- sin_code_bundle/interceptor.py +119 -0
- sin_code_bundle/lsp_backend.py +303 -0
- sin_code_bundle/lsp_bootstrap.py +85 -0
- sin_code_bundle/markitdown.py +254 -0
- sin_code_bundle/mcp_config.py +455 -0
- sin_code_bundle/mcp_server.py +963 -0
- sin_code_bundle/memory.py +208 -0
- sin_code_bundle/merge_safety.py +313 -0
- sin_code_bundle/orchestration_worktrees.py +102 -0
- sin_code_bundle/policy.py +224 -0
- sin_code_bundle/preflight.py +152 -0
- sin_code_bundle/programming_workflow.py +541 -0
- sin_code_bundle/rtk.py +154 -0
- sin_code_bundle/safety.py +52 -0
- sin_code_bundle/session_warmup.py +247 -0
- sin_code_bundle/skills.py +188 -0
- sin_code_bundle/symbol_resolve.py +166 -0
- sin_code_bundle/tools/__init__.py +4 -0
- sin_code_bundle/tools/pypi_setup.py +289 -0
- sin_code_bundle/vfs.py +264 -0
- sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
- sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
- sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
- sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
- sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
- sin_code_bundle-0.9.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Purpose: One-call immortal commit — conventional commit + tag + push.
|
|
2
|
+
|
|
3
|
+
Docs: immortal_commit.doc.md
|
|
4
|
+
|
|
5
|
+
Wraps the git-immortal-commit ritual into a single MCP tool call.
|
|
6
|
+
Validates Conventional Commits format, ensures we are on main (NEVER a
|
|
7
|
+
branch), creates the commit, optionally tags + pushes, and returns a
|
|
8
|
+
structured result.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
# Conventional Commit format: type(scope): subject (subject >= 5 chars).
|
|
22
|
+
# Permitted types mirror the git-immortal-commit skill (AGENTS.md) — adding
|
|
23
|
+
# a new type here means adding it to that skill as well.
|
|
24
|
+
_CC_PATTERN = re.compile(
|
|
25
|
+
r"^(feat|fix|docs|chore|style|test|refactor|perf|ci|build)"
|
|
26
|
+
r"(\([^)]+\))?"
|
|
27
|
+
r": .{5,}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Substrings that should NEVER appear in a commit message (would be a
|
|
31
|
+
# leaked secret in the public history).
|
|
32
|
+
_SECRET_HINTS = (
|
|
33
|
+
"BEGIN RSA PRIVATE KEY",
|
|
34
|
+
"BEGIN OPENSSH PRIVATE KEY",
|
|
35
|
+
"BEGIN PRIVATE KEY",
|
|
36
|
+
"sk-", # OpenAI / many SaaS keys
|
|
37
|
+
"ghp_", # GitHub PAT
|
|
38
|
+
"github_pat_", # GitHub fine-grained PAT
|
|
39
|
+
"xoxb-",
|
|
40
|
+
"xoxp-", # Slack tokens
|
|
41
|
+
"AIza", # Google API keys
|
|
42
|
+
"AKIA", # AWS access key
|
|
43
|
+
"ASIA",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Main-branch names. Repository-specific overrides (e.g. "master") are
|
|
47
|
+
# detected at runtime when the user passes a custom name.
|
|
48
|
+
_DEFAULT_MAIN = "main"
|
|
49
|
+
|
|
50
|
+
# Hard-coded fallback for the dev-machine layout (AGENTS.md). The MCP
|
|
51
|
+
# stdio process inherits a stripped PATH in some envs, so we look up
|
|
52
|
+
# the rollback CLI at well-known locations first.
|
|
53
|
+
_ROLLBACK_FALLBACK = "/Users/jeremy/Library/Python/3.14/bin/sin-honcho-rollback"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ImmortalCommitter:
|
|
57
|
+
"""One-call commit/tag/push with all safety checks applied."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, repo_root: Optional[Path] = None) -> None:
|
|
60
|
+
self.repo_root = Path(repo_root) if repo_root else Path.cwd()
|
|
61
|
+
|
|
62
|
+
def commit(
|
|
63
|
+
self,
|
|
64
|
+
message: str,
|
|
65
|
+
tag: str = "",
|
|
66
|
+
push: bool = True,
|
|
67
|
+
force_main: bool = True,
|
|
68
|
+
main_branch: str = _DEFAULT_MAIN,
|
|
69
|
+
snapshot_first: bool = True,
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""Run the full immortal-commit ritual.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
message: Conventional Commits message (``feat(scope): subject``).
|
|
75
|
+
tag: optional annotated tag name (e.g. ``v0.8.0``).
|
|
76
|
+
push: if True, push commit (and tag) to ``origin/<main_branch>``.
|
|
77
|
+
force_main: if True, refuse to run on any branch other than main.
|
|
78
|
+
main_branch: which branch counts as ``main`` (default ``main``).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict with ``success``, ``sha``, ``tag``, ``pushed``, ``branch``,
|
|
82
|
+
``warnings`` and ``steps`` (per-step status) — always returns
|
|
83
|
+
(never raises) so the caller can surface a useful error.
|
|
84
|
+
"""
|
|
85
|
+
result: Dict[str, Any] = {
|
|
86
|
+
"success": False,
|
|
87
|
+
"message": message,
|
|
88
|
+
"tag": tag or None,
|
|
89
|
+
"pushed": False,
|
|
90
|
+
"branch": None,
|
|
91
|
+
"sha": None,
|
|
92
|
+
"warnings": [],
|
|
93
|
+
"steps": [],
|
|
94
|
+
"snapshot": None,
|
|
95
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ── Step 0 (optional): Pre-commit snapshot ─────────────────────
|
|
99
|
+
# Lets the user roll back to the pre-commit state if the change
|
|
100
|
+
# later turns out to be broken. Independent of the commit itself.
|
|
101
|
+
if snapshot_first:
|
|
102
|
+
snap_name = (
|
|
103
|
+
f"pre-commit-{(message[:24].replace(' ', '-').replace(':', '').lower() or 'auto')}"
|
|
104
|
+
)
|
|
105
|
+
snap = self._create_snapshot(snap_name, message)
|
|
106
|
+
result["snapshot"] = snap
|
|
107
|
+
if snap.get("ok"):
|
|
108
|
+
result["steps"].append(
|
|
109
|
+
{"step": "snapshot", "ok": True, "id": snap.get("snapshot_id")}
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
# Snapshot is best-effort; log but don't fail the commit.
|
|
113
|
+
result["steps"].append(
|
|
114
|
+
{"step": "snapshot", "ok": False, "warning": snap.get("error")}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# ── Step 1: Validate Conventional Commits format ─────────────
|
|
118
|
+
if not _CC_PATTERN.match(message):
|
|
119
|
+
result["error"] = (
|
|
120
|
+
"Not a Conventional Commits message. "
|
|
121
|
+
"Required: type(scope): subject (subject >= 5 chars). "
|
|
122
|
+
"Valid types: feat, fix, docs, chore, style, test, refactor, perf, ci, build."
|
|
123
|
+
)
|
|
124
|
+
result["steps"].append({"step": "validate_format", "ok": False})
|
|
125
|
+
return result
|
|
126
|
+
result["steps"].append({"step": "validate_format", "ok": True})
|
|
127
|
+
|
|
128
|
+
# ── Step 2: Scan message for secrets (cheap, not exhaustive) ──
|
|
129
|
+
secret_hits = [s for s in _SECRET_HINTS if s in message]
|
|
130
|
+
if secret_hits:
|
|
131
|
+
result["error"] = f"Possible secret material in commit message: {secret_hits}"
|
|
132
|
+
result["steps"].append({"step": "secret_scan", "ok": False})
|
|
133
|
+
return result
|
|
134
|
+
result["steps"].append({"step": "secret_scan", "ok": True})
|
|
135
|
+
|
|
136
|
+
# ── Step 3: Detect current branch ─────────────────────────────
|
|
137
|
+
branch = self._git(["branch", "--show-current"], default="").strip()
|
|
138
|
+
result["branch"] = branch
|
|
139
|
+
|
|
140
|
+
if force_main and branch != main_branch:
|
|
141
|
+
result["error"] = (
|
|
142
|
+
f"Refusing to commit: on branch '{branch}', expected '{main_branch}'. "
|
|
143
|
+
"Per the NEVER-BRANCHES mandate, switch to main first: "
|
|
144
|
+
f"`git checkout {main_branch} && git pull origin {main_branch}`."
|
|
145
|
+
)
|
|
146
|
+
result["steps"].append({"step": "branch_check", "ok": False})
|
|
147
|
+
return result
|
|
148
|
+
result["steps"].append({"step": "branch_check", "ok": True, "branch": branch})
|
|
149
|
+
|
|
150
|
+
# ── Step 4: Working-tree dirty? Warn if so, but still proceed ─
|
|
151
|
+
# Per skill: "agents are autonomous, stop blocking on dirty tree,
|
|
152
|
+
# but flag it so the user can see what is being committed together".
|
|
153
|
+
status = self._git(["status", "--porcelain"], default="")
|
|
154
|
+
if status.strip():
|
|
155
|
+
result["warnings"].append(
|
|
156
|
+
f"Working tree is dirty ({len(status.splitlines())} entries) — committing all"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# ── Step 5: git add -A + commit ────────────────────────────────
|
|
160
|
+
add_proc = self._run(["git", "add", "-A"])
|
|
161
|
+
if not add_proc["ok"]:
|
|
162
|
+
result["error"] = f"git add failed: {add_proc['stderr']}"
|
|
163
|
+
result["steps"].append({"step": "git_add", "ok": False})
|
|
164
|
+
return result
|
|
165
|
+
result["steps"].append({"step": "git_add", "ok": True})
|
|
166
|
+
|
|
167
|
+
commit_proc = self._run(["git", "commit", "-m", message])
|
|
168
|
+
if not commit_proc["ok"]:
|
|
169
|
+
# No changes staged is a soft error: surface it but don't
|
|
170
|
+
# treat it as fatal — the user may have wanted a no-op.
|
|
171
|
+
if "nothing to commit" in (commit_proc["stdout"] + commit_proc["stderr"]).lower():
|
|
172
|
+
result["error"] = "Nothing to commit — working tree clean after git add"
|
|
173
|
+
result["steps"].append({"step": "git_commit", "ok": False, "soft": True})
|
|
174
|
+
return result
|
|
175
|
+
result["error"] = f"git commit failed: {commit_proc['stderr']}"
|
|
176
|
+
result["steps"].append({"step": "git_commit", "ok": False})
|
|
177
|
+
return result
|
|
178
|
+
result["steps"].append({"step": "git_commit", "ok": True})
|
|
179
|
+
|
|
180
|
+
# ── Step 6: Capture the new SHA ───────────────────────────────
|
|
181
|
+
sha = self._git(["rev-parse", "HEAD"], default="").strip()
|
|
182
|
+
result["sha"] = sha
|
|
183
|
+
|
|
184
|
+
# ── Step 7: Optional annotated tag ────────────────────────────
|
|
185
|
+
if tag:
|
|
186
|
+
tag_proc = self._run(["git", "tag", "-a", tag, "-m", f"Release {tag}"])
|
|
187
|
+
if not tag_proc["ok"]:
|
|
188
|
+
# If tag already exists, that's a soft error (idempotent).
|
|
189
|
+
if "already exists" in (tag_proc["stderr"]).lower():
|
|
190
|
+
result["warnings"].append(f"Tag '{tag}' already exists locally — keeping")
|
|
191
|
+
else:
|
|
192
|
+
result["error"] = f"git tag failed: {tag_proc['stderr']}"
|
|
193
|
+
result["steps"].append({"step": "git_tag", "ok": False})
|
|
194
|
+
return result
|
|
195
|
+
result["steps"].append({"step": "git_tag", "ok": True})
|
|
196
|
+
|
|
197
|
+
# ── Step 8: Optional push to origin ───────────────────────────
|
|
198
|
+
if push:
|
|
199
|
+
push_proc = self._run(["git", "push", "origin", branch or main_branch])
|
|
200
|
+
if not push_proc["ok"]:
|
|
201
|
+
result["error"] = f"git push failed: {push_proc['stderr']}"
|
|
202
|
+
result["steps"].append({"step": "git_push", "ok": False})
|
|
203
|
+
return result
|
|
204
|
+
result["pushed"] = True
|
|
205
|
+
result["steps"].append({"step": "git_push", "ok": True})
|
|
206
|
+
|
|
207
|
+
if tag:
|
|
208
|
+
# Tags are pushed separately (unless --follow-tags was used).
|
|
209
|
+
tag_push = self._run(["git", "push", "origin", tag])
|
|
210
|
+
if not tag_push["ok"]:
|
|
211
|
+
result["warnings"].append(
|
|
212
|
+
f"Tag '{tag}' was not pushed: {tag_push['stderr'][-200:]}"
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
result["steps"].append({"step": "git_push_tag", "ok": True})
|
|
216
|
+
|
|
217
|
+
result["success"] = True
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
# ── helpers ─────────────────────────────────────────────────────
|
|
221
|
+
def _git(self, args: List[str], default: str = "") -> str:
|
|
222
|
+
"""Run a git command, return stdout (or ``default`` on failure)."""
|
|
223
|
+
proc = subprocess.run(
|
|
224
|
+
["git", *args],
|
|
225
|
+
cwd=self.repo_root,
|
|
226
|
+
capture_output=True,
|
|
227
|
+
text=True,
|
|
228
|
+
timeout=30,
|
|
229
|
+
)
|
|
230
|
+
return proc.stdout if proc.returncode == 0 else default
|
|
231
|
+
|
|
232
|
+
def _run(self, args: List[str]) -> Dict[str, Any]:
|
|
233
|
+
"""Run a subprocess, return dict with ok/stdout/stderr/returncode."""
|
|
234
|
+
try:
|
|
235
|
+
proc = subprocess.run(
|
|
236
|
+
args,
|
|
237
|
+
cwd=self.repo_root,
|
|
238
|
+
capture_output=True,
|
|
239
|
+
text=True,
|
|
240
|
+
timeout=30,
|
|
241
|
+
)
|
|
242
|
+
return {
|
|
243
|
+
"ok": proc.returncode == 0,
|
|
244
|
+
"stdout": proc.stdout,
|
|
245
|
+
"stderr": proc.stderr,
|
|
246
|
+
"returncode": proc.returncode,
|
|
247
|
+
}
|
|
248
|
+
except subprocess.TimeoutExpired:
|
|
249
|
+
return {"ok": False, "stdout": "", "stderr": "timeout after 30s", "returncode": -1}
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
return {"ok": False, "stdout": "", "stderr": str(exc), "returncode": -1}
|
|
252
|
+
|
|
253
|
+
def _create_snapshot(self, name: str, description: str) -> Dict[str, Any]:
|
|
254
|
+
"""Best-effort pre-commit snapshot via sin-honcho-rollback.
|
|
255
|
+
|
|
256
|
+
Never raises. Returns ``{ok, snapshot_id}`` on success, ``{ok: False,
|
|
257
|
+
error}`` on any failure (missing CLI, non-zero exit, JSON parse
|
|
258
|
+
error, timeout).
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
rb_bin = shutil.which("sin-honcho-rollback") or _ROLLBACK_FALLBACK
|
|
262
|
+
if not Path(rb_bin).exists():
|
|
263
|
+
return {"ok": False, "error": "sin-honcho-rollback not installed"}
|
|
264
|
+
proc = subprocess.run(
|
|
265
|
+
[
|
|
266
|
+
rb_bin,
|
|
267
|
+
"snapshot",
|
|
268
|
+
name,
|
|
269
|
+
"--description",
|
|
270
|
+
description or f"pre-commit checkpoint: {name}",
|
|
271
|
+
"--db",
|
|
272
|
+
str(self.repo_root / ".sin" / "rollback.db"),
|
|
273
|
+
],
|
|
274
|
+
capture_output=True,
|
|
275
|
+
text=True,
|
|
276
|
+
timeout=15,
|
|
277
|
+
)
|
|
278
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
279
|
+
data = json.loads(proc.stdout)
|
|
280
|
+
return {
|
|
281
|
+
"ok": True,
|
|
282
|
+
"snapshot_id": data.get("snapshot", {}).get("id") or data.get("id"),
|
|
283
|
+
}
|
|
284
|
+
return {"ok": False, "error": proc.stderr[-300:]}
|
|
285
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
|
|
286
|
+
return {"ok": False, "error": str(exc)}
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
return {"ok": False, "error": str(exc)}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Purpose: ADW interceptor — pre-flight architectural rule enforcement.
|
|
2
|
+
|
|
3
|
+
Docs: interceptor.doc.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── InterceptorRule: Single Pattern Matcher ────────────────────────────────
|
|
14
|
+
class InterceptorRule:
|
|
15
|
+
def __init__(self, name: str, pattern: str, message: str, severity: str = "error"):
|
|
16
|
+
self.name = name
|
|
17
|
+
self.pattern = re.compile(pattern, re.IGNORECASE)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.severity = severity
|
|
20
|
+
|
|
21
|
+
def matches(self, content: str) -> bool:
|
|
22
|
+
return bool(self.pattern.search(content))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── SINInterceptor: Tool-Call Rule Engine ─────────────────────────────────
|
|
26
|
+
class SINInterceptor:
|
|
27
|
+
"""Intercepts tool calls and validates against architectural rules."""
|
|
28
|
+
|
|
29
|
+
DEFAULT_RULES = [
|
|
30
|
+
(
|
|
31
|
+
"no_frontend_db_direct",
|
|
32
|
+
r"(import|require).*(database|db|sql).*from.*(frontend|ui|component)",
|
|
33
|
+
"Frontend components must not import database/SQL modules directly. Use an API layer.",
|
|
34
|
+
"error",
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"no_hardcoded_secrets",
|
|
38
|
+
r"(password|secret|api_key|token)\s*=\s*['\"][^'\"]+['\"]",
|
|
39
|
+
"Hardcoded secrets detected. Use environment variables or a secret manager.",
|
|
40
|
+
"error",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
"no_eval_exec",
|
|
44
|
+
r"\b(eval|exec|subprocess\.shell=True)\b",
|
|
45
|
+
"Dangerous execution pattern (eval/exec/shell=True) detected. Review for injection risks.",
|
|
46
|
+
"warning",
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def __init__(self, repo_root: Optional[Path] = None):
|
|
51
|
+
self.repo_root = repo_root or Path.cwd()
|
|
52
|
+
self.rules: list[InterceptorRule] = []
|
|
53
|
+
self._load_default_rules()
|
|
54
|
+
self._load_adw_rules()
|
|
55
|
+
|
|
56
|
+
def _load_default_rules(self) -> None:
|
|
57
|
+
for name, pattern, message, severity in self.DEFAULT_RULES:
|
|
58
|
+
self.rules.append(InterceptorRule(name, pattern, message, severity))
|
|
59
|
+
|
|
60
|
+
def _load_adw_rules(self) -> None:
|
|
61
|
+
try:
|
|
62
|
+
from sin_code_adw import ADW # type: ignore
|
|
63
|
+
|
|
64
|
+
adw = ADW(repo_root=self.repo_root)
|
|
65
|
+
for rule in adw.get_active_rules():
|
|
66
|
+
self.rules.append(
|
|
67
|
+
InterceptorRule(
|
|
68
|
+
name=rule.get("name", "adw_rule"),
|
|
69
|
+
pattern=rule.get("pattern", ".*"),
|
|
70
|
+
message=rule.get("message", "ADW violation"),
|
|
71
|
+
severity=rule.get("severity", "warning"),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def add_rule(self, name: str, pattern: str, message: str, severity: str = "error") -> None:
|
|
78
|
+
self.rules.append(InterceptorRule(name, pattern, message, severity))
|
|
79
|
+
|
|
80
|
+
def preflight(self, tool_name: str, tool_input: dict) -> dict:
|
|
81
|
+
content = self._extract_content(tool_name, tool_input)
|
|
82
|
+
if not content:
|
|
83
|
+
return {"allowed": True, "violations": []}
|
|
84
|
+
violations = []
|
|
85
|
+
for rule in self.rules:
|
|
86
|
+
if rule.matches(content):
|
|
87
|
+
violations.append(
|
|
88
|
+
{
|
|
89
|
+
"rule": rule.name,
|
|
90
|
+
"message": rule.message,
|
|
91
|
+
"severity": rule.severity,
|
|
92
|
+
"tool": tool_name,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
if violations:
|
|
96
|
+
has_error = any(v["severity"] == "error" for v in violations)
|
|
97
|
+
return {
|
|
98
|
+
"allowed": not has_error,
|
|
99
|
+
"violations": violations,
|
|
100
|
+
"system_reminder": self._format_reminder(violations) if has_error else None,
|
|
101
|
+
}
|
|
102
|
+
return {"allowed": True, "violations": []}
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _extract_content(tool_name: str, tool_input: dict) -> Optional[str]:
|
|
106
|
+
if tool_name in ("sin_write", "sin_edit", "sin_ast_edit"):
|
|
107
|
+
return tool_input.get("content") or tool_input.get("new_content") or ""
|
|
108
|
+
if tool_name == "sin_bash":
|
|
109
|
+
return tool_input.get("command", "")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _format_reminder(violations: list) -> str:
|
|
114
|
+
lines = ["⚠️ **ARCHITECTURAL VIOLATION DETECTED** ⚠️"]
|
|
115
|
+
for v in violations:
|
|
116
|
+
if v["severity"] == "error":
|
|
117
|
+
lines.append(f"- 🚫 [{v['rule'].upper()}] {v['message']}")
|
|
118
|
+
lines.append("\nPlease revise your approach before proceeding.")
|
|
119
|
+
return "\n".join(lines)
|