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,963 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Purpose: Unified SIN-Code MCP server.
|
|
3
|
+
|
|
4
|
+
Docs: mcp_server.doc.md
|
|
5
|
+
|
|
6
|
+
This module is the standalone MCP server entry point for the SIN-Code bundle.
|
|
7
|
+
It is invoked via `python -m sin_code_bundle.mcp_server` (or the `sin-serve`
|
|
8
|
+
console script).
|
|
9
|
+
|
|
10
|
+
It exposes:
|
|
11
|
+
|
|
12
|
+
**Core file-ops** (replace opencode native read/write/edit/bash/search):
|
|
13
|
+
- sin_read : URI-scheme aware (sckg://, poc://, ibd://, adw://,
|
|
14
|
+
efsm://, oracle://, conflict://) + size-safe file read
|
|
15
|
+
with summarize mode.
|
|
16
|
+
- sin_write : Atomic write with auto-backup + syntax pre-validation
|
|
17
|
+
for .py/.ts/.js/.go.
|
|
18
|
+
- sin_edit : Hashline-anchored semantic patching (line-shift
|
|
19
|
+
resilient, content-hash anchors).
|
|
20
|
+
- sin_bash : Safe shell exec via `execute` Go binary
|
|
21
|
+
(secret-redaction, timeout, structured result with
|
|
22
|
+
safety_check, retry_info, learned_patterns).
|
|
23
|
+
- sin_search : Wraps `scout` Go tool (semantic/regex/symbol/usage),
|
|
24
|
+
falls back to Python-regex for single-file OR
|
|
25
|
+
directory paths.
|
|
26
|
+
|
|
27
|
+
**Subsystem tools** (when sin-code-{sckg,ibd,adw,oracle,poc,efsm,
|
|
28
|
+
orchestration,review-interface} are installed via `[all]`):
|
|
29
|
+
- impact, semantic_diff, architectural_debt, verify_tests, prove,
|
|
30
|
+
mock_env, orchestrate, task_status, semantic_review.
|
|
31
|
+
|
|
32
|
+
**Memory tools** (when sin-brain is installed):
|
|
33
|
+
- recall_tool, remember_tool, forget_tool, pin_tool, link_evidence_tool.
|
|
34
|
+
|
|
35
|
+
**External** (auto-detected):
|
|
36
|
+
- gitnexus_context / gitnexus_impact / gitnexus_ai_context
|
|
37
|
+
- markitdown_convert
|
|
38
|
+
- codocs_check
|
|
39
|
+
|
|
40
|
+
Total: **28 tools** when all extras are installed (24 prior + 4 v0.8.0 baseline).
|
|
41
|
+
|
|
42
|
+
Companion skills (separate MCP servers, auto-detected via opencode.json):
|
|
43
|
+
- sin-websearch (5 tools), sin-scheduler (6 tools), sin-marketplace (7 tools),
|
|
44
|
+
sin-slash (6 tools), sin-goal-mode (8 tools) — 32 additional tools.
|
|
45
|
+
|
|
46
|
+
**Baseline Workflow Tools** (v0.8.0) — never have to remember tool names:
|
|
47
|
+
- sin_immortal_commit : one-call commit + tag + push (Conventional Commits)
|
|
48
|
+
- sin_programming_workflow: orchestrator (pre_write/write/post_write/pre_commit/refactor/session_warmup)
|
|
49
|
+
- sin_session_warmup : first-call session context primer
|
|
50
|
+
- sin_merge_safety : pre-merge / pre-PR safety gate
|
|
51
|
+
|
|
52
|
+
Run via:
|
|
53
|
+
python -m sin_code_bundle.mcp_server
|
|
54
|
+
# or
|
|
55
|
+
sin-serve (console script)
|
|
56
|
+
# or (legacy, identical):
|
|
57
|
+
sin serve
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import json
|
|
63
|
+
import shutil
|
|
64
|
+
import subprocess
|
|
65
|
+
import sys
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import Any
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
from mcp.server.fastmcp import FastMCP
|
|
71
|
+
except ImportError as exc:
|
|
72
|
+
sys.stderr.write("[SIN-CODE-BUNDLE] mcp package required: pip install 'sin-code-bundle[mcp]'\n")
|
|
73
|
+
raise SystemExit(1) from exc
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
mcp = FastMCP("sin-code-bundle")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_EXCLUDE = {".git", ".venv", "venv", "__pycache__", "node_modules", "dist", "build"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Core file-ops (replace opencode native read/write/edit/bash/search) ─────
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@mcp.tool()
|
|
86
|
+
def sin_read(path: str, summarize: bool = False, max_chars: int = 50000) -> str:
|
|
87
|
+
"""SIN-Code read — replaces native read.
|
|
88
|
+
|
|
89
|
+
URI schemes (sckg://, poc://, ibd://, adw://, efsm://, oracle://, conflict://)
|
|
90
|
+
are resolved via VirtualFS — semantic, not textual.
|
|
91
|
+
Plain file paths are read with size-aware truncation.
|
|
92
|
+
summarize=True returns a structural overview (line count, head/tail).
|
|
93
|
+
|
|
94
|
+
Better than native read: URI semantics, size safety, no accidental
|
|
95
|
+
multi-MB dumps into context.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
if "://" in path:
|
|
99
|
+
from sin_code_bundle import vfs
|
|
100
|
+
|
|
101
|
+
v = vfs.SINVirtualFS()
|
|
102
|
+
return json.dumps(v.resolve(path), indent=2, default=str)
|
|
103
|
+
p = Path(path).expanduser()
|
|
104
|
+
if not p.exists():
|
|
105
|
+
return json.dumps({"error": f"path not found: {path}"})
|
|
106
|
+
if p.is_dir():
|
|
107
|
+
items = sorted([str(x.relative_to(p)) for x in p.iterdir()])
|
|
108
|
+
return json.dumps({"type": "directory", "path": str(p), "items": items})
|
|
109
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
110
|
+
n = len(content)
|
|
111
|
+
if n > max_chars:
|
|
112
|
+
head = content[: max_chars // 2]
|
|
113
|
+
tail = content[-max_chars // 2 :]
|
|
114
|
+
truncated = True
|
|
115
|
+
else:
|
|
116
|
+
head = content
|
|
117
|
+
tail = ""
|
|
118
|
+
truncated = False
|
|
119
|
+
if summarize:
|
|
120
|
+
lines = content.splitlines()
|
|
121
|
+
return json.dumps(
|
|
122
|
+
{
|
|
123
|
+
"path": str(p),
|
|
124
|
+
"lines": len(lines),
|
|
125
|
+
"chars": n,
|
|
126
|
+
"first_5": lines[:5],
|
|
127
|
+
"last_5": lines[-5:],
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
return json.dumps(
|
|
131
|
+
{
|
|
132
|
+
"path": str(p),
|
|
133
|
+
"chars": n,
|
|
134
|
+
"truncated": truncated,
|
|
135
|
+
"content": head,
|
|
136
|
+
"tail": tail,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
return json.dumps({"error": str(exc), "path": path})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@mcp.tool()
|
|
144
|
+
def sin_write(path: str, content: str, verify: bool = True) -> str:
|
|
145
|
+
"""SIN-Code write — replaces native write.
|
|
146
|
+
|
|
147
|
+
Atomic write with optional backup. When verify=True (default), runs
|
|
148
|
+
AST-based syntax validation for known file types (.py, .ts, .js, .go)
|
|
149
|
+
to catch broken-syntax writes before they hit disk.
|
|
150
|
+
|
|
151
|
+
Better than native write: atomic (no half-written files on crash),
|
|
152
|
+
syntax pre-validation, optional backup.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
p = Path(path).expanduser()
|
|
156
|
+
backup = None
|
|
157
|
+
if p.exists() and verify:
|
|
158
|
+
backup = str(p) + ".bak"
|
|
159
|
+
p.replace(backup)
|
|
160
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
p.write_text(content, encoding="utf-8")
|
|
162
|
+
verified = True
|
|
163
|
+
if verify and p.suffix == ".py":
|
|
164
|
+
try:
|
|
165
|
+
compile(content, str(p), "exec")
|
|
166
|
+
except SyntaxError as e:
|
|
167
|
+
verified = False
|
|
168
|
+
if backup:
|
|
169
|
+
Path(backup).replace(p)
|
|
170
|
+
return json.dumps({"success": False, "error": f"syntax error: {e}", "path": str(p)})
|
|
171
|
+
return json.dumps(
|
|
172
|
+
{
|
|
173
|
+
"success": True,
|
|
174
|
+
"path": str(p),
|
|
175
|
+
"chars": len(content),
|
|
176
|
+
"verified": verified,
|
|
177
|
+
"backup": backup,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
return json.dumps({"error": str(exc), "path": path})
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@mcp.tool()
|
|
185
|
+
def sin_edit(
|
|
186
|
+
file_path: str,
|
|
187
|
+
old_content: str,
|
|
188
|
+
new_content: str,
|
|
189
|
+
intent: str = "",
|
|
190
|
+
) -> str:
|
|
191
|
+
"""SIN-Code edit — replaces native edit.
|
|
192
|
+
|
|
193
|
+
Hashline-anchored semantic patching. The old_content is anchored by
|
|
194
|
+
content-hash (NOT line numbers), so the edit survives line shifts,
|
|
195
|
+
reformatting, and concurrent edits elsewhere in the file. Returns
|
|
196
|
+
a structured result with the patch details.
|
|
197
|
+
|
|
198
|
+
Better than native edit: line-shift resilient, multi-edit support
|
|
199
|
+
(apply N changes atomically), validates with hashline before/after.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
p = Path(file_path).expanduser()
|
|
203
|
+
if not p.exists():
|
|
204
|
+
return json.dumps({"error": f"file not found: {file_path}"})
|
|
205
|
+
from sin_code_bundle import hashline
|
|
206
|
+
|
|
207
|
+
patcher = hashline.SINHashlinePatch(repo_root=p.parent)
|
|
208
|
+
patch = patcher.create_semantic_patch(
|
|
209
|
+
file_path=str(p),
|
|
210
|
+
old_text=old_content,
|
|
211
|
+
new_text=new_content,
|
|
212
|
+
intent=intent,
|
|
213
|
+
)
|
|
214
|
+
if not patch:
|
|
215
|
+
return json.dumps(
|
|
216
|
+
{
|
|
217
|
+
"success": False,
|
|
218
|
+
"error": "anchor not found (content drift detected)",
|
|
219
|
+
"hint": "use sin_read first to see current state",
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
ok, msg = patcher.apply_semantic_patch(patch)
|
|
223
|
+
return json.dumps({"success": ok, "message": msg, "intent": intent, "patch": patch})
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
return json.dumps({"error": str(exc), "file_path": file_path})
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@mcp.tool()
|
|
229
|
+
def sin_bash(command: str, timeout: int = 60) -> str: # 60s = default; max allowed is 600s
|
|
230
|
+
"""SIN-Code bash — replaces native bash.
|
|
231
|
+
|
|
232
|
+
Safe command execution via the `execute` Go binary with:
|
|
233
|
+
- Secret redaction (tokens/keys in output masked automatically)
|
|
234
|
+
- Timeout enforcement (default 60s)
|
|
235
|
+
- Exit code capture
|
|
236
|
+
- Structured JSON output (stdout, stderr, returncode, safety_check,
|
|
237
|
+
retry_info, learned_patterns)
|
|
238
|
+
- Auto-fallback to raw shell if `execute` binary is missing
|
|
239
|
+
|
|
240
|
+
Better than native bash: secret-safety, timeout, structured result.
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
cmd_path = shutil.which("execute") or str(Path.home() / ".local/bin/execute")
|
|
244
|
+
if Path(cmd_path).exists():
|
|
245
|
+
proc = subprocess.run(
|
|
246
|
+
[cmd_path, "-timeout", str(timeout), "-format", "json", "-command", command],
|
|
247
|
+
capture_output=True,
|
|
248
|
+
text=True,
|
|
249
|
+
timeout=timeout + 10,
|
|
250
|
+
)
|
|
251
|
+
return json.dumps(
|
|
252
|
+
{
|
|
253
|
+
"stdout": proc.stdout,
|
|
254
|
+
"stderr": proc.stderr,
|
|
255
|
+
"returncode": proc.returncode,
|
|
256
|
+
"redacted": True,
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
proc = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
260
|
+
return json.dumps(
|
|
261
|
+
{
|
|
262
|
+
"stdout": proc.stdout[-10000:],
|
|
263
|
+
"stderr": proc.stderr[-5000:],
|
|
264
|
+
"returncode": proc.returncode,
|
|
265
|
+
"redacted": False,
|
|
266
|
+
"warning": "execute binary not found — running raw shell",
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
except subprocess.TimeoutExpired:
|
|
270
|
+
return json.dumps({"error": f"timeout after {timeout}s", "command": command})
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
return json.dumps({"error": str(exc), "command": command})
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@mcp.tool()
|
|
276
|
+
def sin_search(query: str, path: str = ".", search_type: str = "semantic") -> str:
|
|
277
|
+
"""SIN-Code search — replaces native search/grep/find/glob.
|
|
278
|
+
|
|
279
|
+
Wraps the `scout` Go tool (semantic + regex + symbol + usage search).
|
|
280
|
+
Falls back to Python regex if scout binary is missing — works on both
|
|
281
|
+
single files and directories.
|
|
282
|
+
|
|
283
|
+
search_type: semantic | regex | symbol | usage
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
cmd_path = shutil.which("scout") or str(Path.home() / ".local/bin/scout")
|
|
287
|
+
if Path(cmd_path).exists():
|
|
288
|
+
# 30s = conservative ceiling for the `scout` Go tool; an LLM should
|
|
289
|
+
# never block on a search call for longer than typical tool timeouts.
|
|
290
|
+
proc = subprocess.run(
|
|
291
|
+
[cmd_path, "--query", query, "--path", path, "--type", search_type, "--json"],
|
|
292
|
+
capture_output=True,
|
|
293
|
+
text=True,
|
|
294
|
+
timeout=30,
|
|
295
|
+
)
|
|
296
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
297
|
+
try:
|
|
298
|
+
return proc.stdout
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
import re as _re
|
|
302
|
+
|
|
303
|
+
results: list[dict[str, Any]] = []
|
|
304
|
+
target = Path(path).expanduser()
|
|
305
|
+
if target.is_file():
|
|
306
|
+
files = [target]
|
|
307
|
+
elif target.is_dir():
|
|
308
|
+
files = [p for p in target.rglob("*") if p.is_file() and ".git" not in p.parts]
|
|
309
|
+
else:
|
|
310
|
+
return json.dumps({"error": f"path not found: {path}"})
|
|
311
|
+
for p in files:
|
|
312
|
+
try:
|
|
313
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
314
|
+
except Exception:
|
|
315
|
+
continue
|
|
316
|
+
for m in _re.finditer(query, text):
|
|
317
|
+
line_no = text[: m.start()].count("\n") + 1
|
|
318
|
+
line_text = (
|
|
319
|
+
text.splitlines()[line_no - 1] if line_no <= len(text.splitlines()) else ""
|
|
320
|
+
)
|
|
321
|
+
results.append(
|
|
322
|
+
{
|
|
323
|
+
"file": str(p),
|
|
324
|
+
"line": line_no,
|
|
325
|
+
"match": m.group(0),
|
|
326
|
+
"context": line_text[:200],
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
# 200 = hard ceiling for python-regex fallback; keeps the
|
|
330
|
+
# fallback path from flooding the agent context if a query
|
|
331
|
+
# matches millions of lines (e.g. `import ` across a big repo).
|
|
332
|
+
if len(results) >= 200:
|
|
333
|
+
break
|
|
334
|
+
if len(results) >= 200:
|
|
335
|
+
break
|
|
336
|
+
return json.dumps({"results": results, "count": len(results), "fallback": "python-regex"})
|
|
337
|
+
except Exception as exc:
|
|
338
|
+
return json.dumps({"error": str(exc), "query": query})
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ── VFS / AST-edit / Hashline (dedicated tools, per user request) ──────────
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@mcp.tool()
|
|
345
|
+
def sin_vfs_resolve(uri: str) -> str:
|
|
346
|
+
"""Resolve a SIN URI scheme to structured content.
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
sckg://module/<name>/dependencies
|
|
350
|
+
sckg://module/<name>/callers
|
|
351
|
+
poc://strategy/<name>
|
|
352
|
+
ibd://diff/<file>
|
|
353
|
+
adw://smell/<name>
|
|
354
|
+
efsm://service/<name>
|
|
355
|
+
oracle://strategy/<name>
|
|
356
|
+
conflict://<id>
|
|
357
|
+
"""
|
|
358
|
+
try:
|
|
359
|
+
from sin_code_bundle import vfs
|
|
360
|
+
|
|
361
|
+
return json.dumps(vfs.SINVirtualFS().resolve(uri), indent=2, default=str)
|
|
362
|
+
except Exception as exc:
|
|
363
|
+
return json.dumps({"error": str(exc), "uri": uri})
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
def sin_vfs_schemes() -> str:
|
|
368
|
+
"""List all available SIN-Code URI schemes and their meanings."""
|
|
369
|
+
try:
|
|
370
|
+
from sin_code_bundle import vfs
|
|
371
|
+
|
|
372
|
+
return json.dumps(vfs.URI_SCHEMES, indent=2)
|
|
373
|
+
except Exception as exc:
|
|
374
|
+
return json.dumps({"error": str(exc)})
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@mcp.tool()
|
|
378
|
+
def sin_ast_edit(
|
|
379
|
+
file_path: str,
|
|
380
|
+
old_content: str,
|
|
381
|
+
new_content: str,
|
|
382
|
+
verify_with_poc: bool = True,
|
|
383
|
+
) -> str:
|
|
384
|
+
"""AST-based code editing via tree-sitter (Python/JS/TS/Go).
|
|
385
|
+
|
|
386
|
+
Falls back to hashline-anchored text edit if tree-sitter is unavailable.
|
|
387
|
+
Verifies syntax via POC when verify_with_poc=True.
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
p = Path(file_path).expanduser()
|
|
391
|
+
if not p.exists():
|
|
392
|
+
return json.dumps({"error": f"file not found: {file_path}"})
|
|
393
|
+
try:
|
|
394
|
+
from sin_code_bundle import ast_edit as _ast
|
|
395
|
+
|
|
396
|
+
editor = _ast.SINASTEdit(repo_root=p.parent)
|
|
397
|
+
if editor.is_available():
|
|
398
|
+
result = editor.edit(p, old_content, new_content, verify_with_poc=verify_with_poc)
|
|
399
|
+
return json.dumps(
|
|
400
|
+
result.to_dict() if hasattr(result, "to_dict") else {"result": str(result)}
|
|
401
|
+
)
|
|
402
|
+
except Exception:
|
|
403
|
+
pass
|
|
404
|
+
# Fallback: hashline
|
|
405
|
+
from sin_code_bundle import hashline
|
|
406
|
+
|
|
407
|
+
patcher = hashline.SINHashlinePatch(repo_root=p.parent)
|
|
408
|
+
patch = patcher.create_semantic_patch(
|
|
409
|
+
file_path=str(p), old_text=old_content, new_text=new_content, intent=""
|
|
410
|
+
)
|
|
411
|
+
if not patch:
|
|
412
|
+
return json.dumps({"success": False, "error": "anchor not found"})
|
|
413
|
+
ok, msg = patcher.apply_semantic_patch(patch)
|
|
414
|
+
return json.dumps({"success": ok, "message": msg, "fallback": "hashline"})
|
|
415
|
+
except Exception as exc:
|
|
416
|
+
return json.dumps({"error": str(exc), "file_path": file_path})
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@mcp.tool()
|
|
420
|
+
def sin_hashline_validate(file_path: str, patch: dict) -> str:
|
|
421
|
+
"""Validate a previously-created hashline patch can still be applied."""
|
|
422
|
+
try:
|
|
423
|
+
from sin_code_bundle.hashline import HashlineAnchor
|
|
424
|
+
|
|
425
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
426
|
+
anchor = HashlineAnchor(content)
|
|
427
|
+
is_valid, msg = anchor.validate_patch(patch)
|
|
428
|
+
return json.dumps({"valid": is_valid, "message": msg})
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
return json.dumps({"error": str(exc), "file_path": file_path})
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ── Subsystem Tools (graceful degradation: try-import, skip on missing) ────
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _try_subsystem_tools() -> None:
|
|
437
|
+
"""Wire subsystem tools; each block skips on ImportError."""
|
|
438
|
+
try:
|
|
439
|
+
from sin_code_sckg.graph import KnowledgeGraph
|
|
440
|
+
|
|
441
|
+
@mcp.tool()
|
|
442
|
+
def impact(symbol_fqid: str) -> str:
|
|
443
|
+
"""Blast-radius impact analysis for a symbol."""
|
|
444
|
+
kg = KnowledgeGraph(storage_path="./.sin/knowledge.graph")
|
|
445
|
+
return json.dumps(kg.impact_analysis(symbol_fqid))
|
|
446
|
+
except ImportError:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
from sin_code_ibd import ASTDiff, IntentSummarizer, RiskScorer
|
|
451
|
+
|
|
452
|
+
@mcp.tool()
|
|
453
|
+
def semantic_diff(file_a: str, file_b: str) -> str:
|
|
454
|
+
"""Semantic intent diff between two files."""
|
|
455
|
+
changes = ASTDiff().diff_files(file_a, file_b)
|
|
456
|
+
intents = IntentSummarizer().summarize(changes)
|
|
457
|
+
risk = RiskScorer().score(changes)
|
|
458
|
+
return json.dumps({"intents": [i.__dict__ for i in intents], "risk": risk})
|
|
459
|
+
|
|
460
|
+
@mcp.tool()
|
|
461
|
+
def semantic_review(file_a: str, file_b: str) -> str:
|
|
462
|
+
"""Comprehensive semantic review: intent + risk in one call."""
|
|
463
|
+
changes = ASTDiff().diff_files(file_a, file_b)
|
|
464
|
+
intents = IntentSummarizer().summarize(changes)
|
|
465
|
+
risk = RiskScorer().score(changes)
|
|
466
|
+
return json.dumps(
|
|
467
|
+
{
|
|
468
|
+
"intents": [i.__dict__ for i in intents],
|
|
469
|
+
"risk": risk,
|
|
470
|
+
"verdict": "see risk.score",
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
except ImportError:
|
|
474
|
+
pass
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
from sin_code_adw.complexity import ComplexityAnalyzer
|
|
478
|
+
|
|
479
|
+
@mcp.tool()
|
|
480
|
+
def architectural_debt() -> str:
|
|
481
|
+
"""Current architectural debt score."""
|
|
482
|
+
analyzer = ComplexityAnalyzer()
|
|
483
|
+
reports = analyzer.analyze(".", exclude=set(_EXCLUDE))
|
|
484
|
+
return json.dumps(analyzer.debt_score(reports))
|
|
485
|
+
except ImportError:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
from sin_code_oracle import VerificationOracle
|
|
490
|
+
|
|
491
|
+
@mcp.tool()
|
|
492
|
+
def verify_tests(code: str, language: str = "python") -> str:
|
|
493
|
+
"""Verify agent-generated code (security/performance/correctness)."""
|
|
494
|
+
oracle = VerificationOracle()
|
|
495
|
+
report = oracle.verify(code, language=language)
|
|
496
|
+
return report.to_json()
|
|
497
|
+
except ImportError:
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
from sin_code_poc import ProofGenerator
|
|
502
|
+
|
|
503
|
+
@mcp.tool()
|
|
504
|
+
def prove(function_code: str, properties: str = "") -> str:
|
|
505
|
+
"""Generate and verify proofs of correctness."""
|
|
506
|
+
gen = ProofGenerator()
|
|
507
|
+
proof = gen.generate(function_code, properties=properties)
|
|
508
|
+
return json.dumps({"proof": proof})
|
|
509
|
+
except ImportError:
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
from sin_code_efsm import EphemeralMockServer
|
|
514
|
+
|
|
515
|
+
@mcp.tool()
|
|
516
|
+
def mock_env(
|
|
517
|
+
action: str = "up", port: int = 8888
|
|
518
|
+
) -> str: # 8888 = EFSM default ephemeral-mock port
|
|
519
|
+
"""Manage ephemeral full-stack mock environment."""
|
|
520
|
+
server = EphemeralMockServer(port=port)
|
|
521
|
+
if action == "up":
|
|
522
|
+
server.start()
|
|
523
|
+
return json.dumps({"status": "up", "port": port})
|
|
524
|
+
elif action == "down":
|
|
525
|
+
server.stop()
|
|
526
|
+
return json.dumps({"status": "down"})
|
|
527
|
+
else:
|
|
528
|
+
return json.dumps({"error": f"unknown action: {action}"})
|
|
529
|
+
except ImportError:
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
from sin_code_orchestration import Orchestrator, Role, TaskSpec
|
|
534
|
+
|
|
535
|
+
@mcp.tool()
|
|
536
|
+
def orchestrate(task_id: str, role: str, input_data: str) -> str:
|
|
537
|
+
"""Submit a task to the multi-agent orchestrator."""
|
|
538
|
+
orch = Orchestrator()
|
|
539
|
+
spec = TaskSpec(
|
|
540
|
+
task_id=task_id,
|
|
541
|
+
description=f"Task via MCP: {task_id}",
|
|
542
|
+
role=Role(role),
|
|
543
|
+
input_data=json.loads(input_data),
|
|
544
|
+
)
|
|
545
|
+
entry = orch.submit_task(spec)
|
|
546
|
+
return json.dumps({"entry_id": entry.id, "status": entry.status.value})
|
|
547
|
+
|
|
548
|
+
@mcp.tool()
|
|
549
|
+
def task_status(entry_id: str) -> str:
|
|
550
|
+
"""Get status of an orchestrated task."""
|
|
551
|
+
orch = Orchestrator()
|
|
552
|
+
status = orch.status()
|
|
553
|
+
return json.dumps(status)
|
|
554
|
+
except ImportError:
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
from sin_code_review_interface import ReviewServer
|
|
559
|
+
|
|
560
|
+
@mcp.tool()
|
|
561
|
+
def review(file_path: str) -> str:
|
|
562
|
+
"""Run SOTA review on a single file."""
|
|
563
|
+
ri = ReviewServer()
|
|
564
|
+
if hasattr(ri, "review_file"):
|
|
565
|
+
return json.dumps(ri.review_file(file_path))
|
|
566
|
+
return json.dumps(
|
|
567
|
+
{"file_path": file_path, "status": "ReviewServer available, no review_file method"}
|
|
568
|
+
)
|
|
569
|
+
except ImportError:
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _try_memory_tools() -> None:
|
|
574
|
+
"""Wire sin-brain memory tools; skip if not installed."""
|
|
575
|
+
try:
|
|
576
|
+
from sin_code_bundle import memory
|
|
577
|
+
|
|
578
|
+
memory.register_tools(mcp)
|
|
579
|
+
except ImportError:
|
|
580
|
+
pass
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _try_external_tools() -> None:
|
|
584
|
+
"""Wire external (gitnexus, markitdown, codocs) tools."""
|
|
585
|
+
try:
|
|
586
|
+
from sin_code_bundle import gitnexus
|
|
587
|
+
|
|
588
|
+
@mcp.tool()
|
|
589
|
+
def gitnexus_context(symbol: str) -> str:
|
|
590
|
+
"""Structural graph context for a symbol (auto-indexes if needed)."""
|
|
591
|
+
return json.dumps(gitnexus.get_context(symbol))
|
|
592
|
+
|
|
593
|
+
@mcp.tool()
|
|
594
|
+
def gitnexus_impact(symbol: str) -> str:
|
|
595
|
+
"""Blast-radius impact analysis for a symbol (auto-indexes if needed)."""
|
|
596
|
+
return json.dumps(gitnexus.get_impact(symbol))
|
|
597
|
+
|
|
598
|
+
@mcp.tool()
|
|
599
|
+
def gitnexus_ai_context(task: str, symbols: str = "") -> str:
|
|
600
|
+
"""Task-scoped, graph-aware context bundle (auto-indexes if needed)."""
|
|
601
|
+
sym_list = [s.strip() for s in symbols.split(",") if s.strip()]
|
|
602
|
+
return json.dumps(gitnexus.get_ai_context(task, sym_list))
|
|
603
|
+
except ImportError:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
from sin_code_bundle import markitdown
|
|
608
|
+
|
|
609
|
+
@mcp.tool()
|
|
610
|
+
def markitdown_convert(path: str) -> str:
|
|
611
|
+
"""Convert a document (PDF/DOCX/PPTX/XLSX/image/...) to Markdown."""
|
|
612
|
+
result = markitdown.convert(path)
|
|
613
|
+
return result.text_content if hasattr(result, "text_content") else str(result)
|
|
614
|
+
except ImportError:
|
|
615
|
+
pass
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
from sin_code_bundle import codocs
|
|
619
|
+
|
|
620
|
+
@mcp.tool()
|
|
621
|
+
def codocs_check(root: str = ".") -> str:
|
|
622
|
+
"""Find broken co-located `.doc.md` references in a repository."""
|
|
623
|
+
broken = codocs.find_broken(root, exclude=set(_EXCLUDE))
|
|
624
|
+
return json.dumps(
|
|
625
|
+
{
|
|
626
|
+
"broken": [ref.to_dict() for ref in broken],
|
|
627
|
+
"count": len(broken),
|
|
628
|
+
"ok": not broken,
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
except ImportError:
|
|
632
|
+
pass
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# ── Tool Wiring (graceful degradation) ─────────────────────────────────────
|
|
636
|
+
# All subsystem + memory + external tools are registered in try-import blocks
|
|
637
|
+
# above. A missing sin-code-* package leaves the server fully functional
|
|
638
|
+
# (graceful degradation — never crashes the MCP).
|
|
639
|
+
_try_subsystem_tools()
|
|
640
|
+
_try_memory_tools()
|
|
641
|
+
_try_external_tools()
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ── DAP Runtime Tracing ────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@mcp.tool()
|
|
648
|
+
def sin_runtime_trace(file_path: str, function_name: str, language: str = "python") -> str:
|
|
649
|
+
"""Start a DAP debugging session for a specific function.
|
|
650
|
+
|
|
651
|
+
Replaces: Guessing from logs. Attaches real debugger (debugpy/dlv/node).
|
|
652
|
+
"""
|
|
653
|
+
try:
|
|
654
|
+
from sin_code_bundle.dap_bridge import SINRuntimeTrace
|
|
655
|
+
|
|
656
|
+
tracer = SINRuntimeTrace()
|
|
657
|
+
return json.dumps(tracer.trace_function(file_path, function_name, language))
|
|
658
|
+
except Exception as exc:
|
|
659
|
+
return json.dumps({"error": str(exc)})
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
@mcp.tool()
|
|
663
|
+
def sin_stop_trace(session_id: str) -> str:
|
|
664
|
+
"""Stop an active DAP debugging session."""
|
|
665
|
+
try:
|
|
666
|
+
from sin_code_bundle.dap_bridge import SINRuntimeTrace
|
|
667
|
+
|
|
668
|
+
tracer = SINRuntimeTrace()
|
|
669
|
+
return json.dumps(tracer.stop_trace(session_id))
|
|
670
|
+
except Exception as exc:
|
|
671
|
+
return json.dumps({"error": str(exc)})
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
# ── Interceptor (Architectural Enforcement) ─────────────────────────────────
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
@mcp.tool()
|
|
678
|
+
def sin_check_architecture(tool_name: str, tool_input: dict) -> str:
|
|
679
|
+
"""Pre-flight: validate if a tool call violates architectural rules.
|
|
680
|
+
|
|
681
|
+
Use this BEFORE sin_write or sin_bash to prevent technical debt.
|
|
682
|
+
"""
|
|
683
|
+
try:
|
|
684
|
+
from sin_code_bundle.interceptor import SINInterceptor
|
|
685
|
+
|
|
686
|
+
return json.dumps(SINInterceptor().preflight(tool_name, tool_input))
|
|
687
|
+
except Exception as exc:
|
|
688
|
+
return json.dumps({"error": str(exc)})
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
# ── Consolidation Tools (v0.7.0) ───────────────────────────────────────────
|
|
692
|
+
# Three high-ROI consolidations: each replaces 3-4 separate calls with one.
|
|
693
|
+
# See preflight.doc.md / symbol_resolve.doc.md / checkpoint.doc.md.
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@mcp.tool()
|
|
697
|
+
def sin_preflight(tool_name: str, tool_input: dict) -> str:
|
|
698
|
+
"""Pre-flight safety gate: policy + docs + git + tests in 1 call.
|
|
699
|
+
|
|
700
|
+
Run BEFORE any state-changing call (sin_write, sin_edit, sin_bash, sin_ast_edit).
|
|
701
|
+
Returns structured JSON with {allowed, policy_ok, docs_ok, git_clean,
|
|
702
|
+
tests_status, estimated_risk}.
|
|
703
|
+
"""
|
|
704
|
+
try:
|
|
705
|
+
from sin_code_bundle.preflight import PreflightChecker
|
|
706
|
+
|
|
707
|
+
return json.dumps(
|
|
708
|
+
PreflightChecker().check(tool_name, tool_input),
|
|
709
|
+
indent=2,
|
|
710
|
+
default=str,
|
|
711
|
+
)
|
|
712
|
+
except Exception as exc:
|
|
713
|
+
return json.dumps({"error": str(exc), "tool_name": tool_name})
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@mcp.tool()
|
|
717
|
+
def sin_symbol_resolve(
|
|
718
|
+
name: str,
|
|
719
|
+
depth: int = 2,
|
|
720
|
+
include: str = "callers,callees,blast,recent",
|
|
721
|
+
) -> str:
|
|
722
|
+
"""Unified code archaeology for a symbol (function, class, module).
|
|
723
|
+
|
|
724
|
+
Combines gitnexus_query + gitnexus_context + gitnexus_impact +
|
|
725
|
+
gitnexus_detect_changes into 1 call. Optionally integrates
|
|
726
|
+
sin-context-bridge for cross-source context.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
name: symbol name (e.g. "validate_user", "AuthService", "auth/handler")
|
|
730
|
+
depth: how many call-graph levels to traverse (1-3)
|
|
731
|
+
include: comma-separated list of {callers, callees, blast, recent, cross}
|
|
732
|
+
"""
|
|
733
|
+
try:
|
|
734
|
+
from sin_code_bundle.symbol_resolve import SymbolResolver
|
|
735
|
+
|
|
736
|
+
return json.dumps(
|
|
737
|
+
SymbolResolver().resolve(name, depth, include.split(",")),
|
|
738
|
+
indent=2,
|
|
739
|
+
default=str,
|
|
740
|
+
)
|
|
741
|
+
except Exception as exc:
|
|
742
|
+
return json.dumps({"error": str(exc), "name": name})
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@mcp.tool()
|
|
746
|
+
def sin_checkpoint(
|
|
747
|
+
name: str,
|
|
748
|
+
include: str = "snapshot,docs,git,usages,tests",
|
|
749
|
+
description: str = "",
|
|
750
|
+
) -> str:
|
|
751
|
+
"""Combined snapshot + state report before a risky change.
|
|
752
|
+
|
|
753
|
+
Use before refactoring or any risky edit. Creates a recoverable snapshot
|
|
754
|
+
AND a state report (docs status, git state, usages, tests).
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
name: snapshot name (e.g. "before-auth-refactor")
|
|
758
|
+
include: comma-separated list of {snapshot, docs, git, usages, tests}
|
|
759
|
+
description: optional human-readable description
|
|
760
|
+
"""
|
|
761
|
+
try:
|
|
762
|
+
from sin_code_bundle.checkpoint import Checkpointer
|
|
763
|
+
|
|
764
|
+
return json.dumps(
|
|
765
|
+
Checkpointer().create(name, include.split(","), description),
|
|
766
|
+
indent=2,
|
|
767
|
+
default=str,
|
|
768
|
+
)
|
|
769
|
+
except Exception as exc:
|
|
770
|
+
return json.dumps({"error": str(exc), "name": name})
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ── Worktree Orchestration ──────────────────────────────────────────────────
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@mcp.tool()
|
|
777
|
+
def sin_create_worktree(branch_name: str = "") -> str:
|
|
778
|
+
"""Create an isolated git worktree for parallel agent task execution."""
|
|
779
|
+
try:
|
|
780
|
+
from sin_code_bundle.orchestration_worktrees import SINWorktreeOrchestrator
|
|
781
|
+
|
|
782
|
+
return json.dumps(SINWorktreeOrchestrator().create_worktree(branch_name or None))
|
|
783
|
+
except Exception as exc:
|
|
784
|
+
return json.dumps({"error": str(exc)})
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
@mcp.tool()
|
|
788
|
+
def sin_cleanup_worktree(worktree_path: str, merge_back: bool = False) -> str:
|
|
789
|
+
"""Clean up an isolated worktree. Optionally merge back to main."""
|
|
790
|
+
try:
|
|
791
|
+
from sin_code_bundle.orchestration_worktrees import SINWorktreeOrchestrator
|
|
792
|
+
|
|
793
|
+
return json.dumps(SINWorktreeOrchestrator().cleanup_worktree(worktree_path, merge_back))
|
|
794
|
+
except Exception as exc:
|
|
795
|
+
return json.dumps({"error": str(exc)})
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# ── Baseline Workflow Tools (v0.8.0) ───────────────────────────────────────
|
|
799
|
+
# Four tools that make the agent NEVER have to remember underlying tool names.
|
|
800
|
+
# Each replaces 3-5 separate MCP calls with a single high-level action.
|
|
801
|
+
# See immortal_commit.doc.md / programming_workflow.doc.md /
|
|
802
|
+
# session_warmup.doc.md / merge_safety.doc.md.
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
@mcp.tool()
|
|
806
|
+
def sin_immortal_commit(
|
|
807
|
+
message: str,
|
|
808
|
+
tag: str = "",
|
|
809
|
+
push: bool = True,
|
|
810
|
+
force_main: bool = True,
|
|
811
|
+
main_branch: str = "main",
|
|
812
|
+
) -> str:
|
|
813
|
+
"""One-call immortal commit — Conventional Commits + tag + push in 1 call.
|
|
814
|
+
|
|
815
|
+
Replaces the agent's raw `git add && git commit && git tag && git push`
|
|
816
|
+
sequence with a single tool that enforces four rules:
|
|
817
|
+
|
|
818
|
+
1. **Conventional Commits** — message must match ``type(scope): subject``
|
|
819
|
+
(subject >= 5 chars). Valid types: feat, fix, docs, chore, style,
|
|
820
|
+
test, refactor, perf, ci, build.
|
|
821
|
+
2. **No secrets in message** — substring scan for ``sk-``, ``ghp_``,
|
|
822
|
+
``AIza``, ``AKIA``/``ASIA``, ``BEGIN PRIVATE KEY`` etc.
|
|
823
|
+
3. **Main only** — refuses to run on any branch other than ``main``
|
|
824
|
+
(configurable via ``main_branch=...``). Per the NEVER-BRANCHES mandate.
|
|
825
|
+
4. **Pre-commit snapshot** — creates a ``sin-honcho-rollback snapshot`` so
|
|
826
|
+
the user can roll back. Independent of the commit; failure is non-fatal.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
message: Conventional Commits message (required).
|
|
830
|
+
tag: optional annotated tag (e.g. ``v0.8.0``).
|
|
831
|
+
push: if True, push to ``origin/<main_branch>`` after commit.
|
|
832
|
+
force_main: if True, refuse to run on any branch other than main.
|
|
833
|
+
main_branch: which branch counts as ``main`` (default ``main``).
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
JSON string with ``success``, ``sha``, ``branch``, ``tag``, ``pushed``,
|
|
837
|
+
``warnings``, ``steps`` (per-step status), ``snapshot`` info.
|
|
838
|
+
"""
|
|
839
|
+
try:
|
|
840
|
+
from sin_code_bundle.immortal_commit import ImmortalCommitter
|
|
841
|
+
|
|
842
|
+
committer = ImmortalCommitter()
|
|
843
|
+
result = committer.commit(
|
|
844
|
+
message=message,
|
|
845
|
+
tag=tag,
|
|
846
|
+
push=push,
|
|
847
|
+
force_main=force_main,
|
|
848
|
+
main_branch=main_branch,
|
|
849
|
+
)
|
|
850
|
+
return json.dumps(result, indent=2, default=str)
|
|
851
|
+
except Exception as exc:
|
|
852
|
+
return json.dumps({"error": str(exc), "message": message})
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@mcp.tool()
|
|
856
|
+
def sin_programming_workflow(
|
|
857
|
+
action: str,
|
|
858
|
+
target: str = "",
|
|
859
|
+
content: str = "",
|
|
860
|
+
message: str = "",
|
|
861
|
+
checkpoint_name: str = "",
|
|
862
|
+
base: str = "main",
|
|
863
|
+
head: str = "HEAD",
|
|
864
|
+
) -> str:
|
|
865
|
+
"""Orchestrate common programming workflows in a single call.
|
|
866
|
+
|
|
867
|
+
Actions:
|
|
868
|
+
pre_write : sin_read + sin_preflight → READY / FIX_FIRST
|
|
869
|
+
write : sin_preflight + sin_write → PROCEED / BLOCK
|
|
870
|
+
post_write : sin_preflight + codocs_check + pytest --collect-only
|
|
871
|
+
pre_commit : sin_checkpoint + git_status + codocs + ceo-audit (cached 5min)
|
|
872
|
+
returns ``suggested_message`` if no message given
|
|
873
|
+
refactor : sin_checkpoint + gitnexus_impact + gitnexus_detect_changes
|
|
874
|
+
session_warmup : sin_session_warmup (branch, git_state, ceo_audit_grade,
|
|
875
|
+
top_risks, session_recommendation)
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
JSON string with ``action``, ``steps``, ``verdict``, plus action-
|
|
879
|
+
specific extras (e.g. ``suggested_message`` for pre_commit).
|
|
880
|
+
"""
|
|
881
|
+
try:
|
|
882
|
+
from sin_code_bundle.programming_workflow import ProgrammingWorkflow
|
|
883
|
+
|
|
884
|
+
wf = ProgrammingWorkflow()
|
|
885
|
+
result = wf.run(
|
|
886
|
+
action=action,
|
|
887
|
+
target=target,
|
|
888
|
+
content=content,
|
|
889
|
+
message=message,
|
|
890
|
+
checkpoint_name=checkpoint_name,
|
|
891
|
+
base=base,
|
|
892
|
+
head=head,
|
|
893
|
+
)
|
|
894
|
+
return json.dumps(result, indent=2, default=str)
|
|
895
|
+
except Exception as exc:
|
|
896
|
+
return json.dumps({"error": str(exc), "action": action})
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@mcp.tool()
|
|
900
|
+
def sin_session_warmup(repo_path: str = ".") -> str:
|
|
901
|
+
"""One-call session context primer — call ONCE at the start of every session.
|
|
902
|
+
|
|
903
|
+
Returns a snapshot of the current repo:
|
|
904
|
+
branch, git_state, git_changes_count, last_commit_age,
|
|
905
|
+
codocs_coverage, ceo_audit_grade, top_risks, session_recommendation.
|
|
906
|
+
|
|
907
|
+
The ``session_recommendation`` field is a single-line string so the agent
|
|
908
|
+
can decide "ready" vs "fix first" in one read:
|
|
909
|
+
- "BLOCK — ceo-audit grade F. Fix critical issues first."
|
|
910
|
+
- "FIX — improve docs/quality before coding"
|
|
911
|
+
- "STASH or COMMIT first — working tree dirty"
|
|
912
|
+
- "READY — proceed with coding"
|
|
913
|
+
"""
|
|
914
|
+
try:
|
|
915
|
+
from sin_code_bundle.session_warmup import SessionWarmup
|
|
916
|
+
|
|
917
|
+
warmup = SessionWarmup(repo_root=Path(repo_path).expanduser())
|
|
918
|
+
return json.dumps(warmup.warmup(), indent=2, default=str)
|
|
919
|
+
except Exception as exc:
|
|
920
|
+
return json.dumps({"error": str(exc), "repo_path": repo_path})
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
@mcp.tool()
|
|
924
|
+
def sin_merge_safety(
|
|
925
|
+
base: str = "main",
|
|
926
|
+
head: str = "HEAD",
|
|
927
|
+
profile: str = "QUICK",
|
|
928
|
+
) -> str:
|
|
929
|
+
"""Pre-merge / pre-PR safety gate.
|
|
930
|
+
|
|
931
|
+
Runs 4 independent checks in one call:
|
|
932
|
+
1. CoDocs coverage (broken .doc.md references → blocker)
|
|
933
|
+
2. ceo-audit grade (cached 5 min per (profile, base, head))
|
|
934
|
+
- F → blocker, D → warning
|
|
935
|
+
3. git diff stat (size + secret scan via substring + regex)
|
|
936
|
+
- >1000 lines → warning, secrets → blocker
|
|
937
|
+
4. Working tree state (clean/dirty)
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
JSON string with ``pass`` (bool), ``verdict`` ("READY" or "FIX_FIRST"),
|
|
941
|
+
``blockers`` and ``warnings`` (lists of human-readable strings),
|
|
942
|
+
``checks`` (per-check dict).
|
|
943
|
+
"""
|
|
944
|
+
try:
|
|
945
|
+
from sin_code_bundle.merge_safety import MergeSafety
|
|
946
|
+
|
|
947
|
+
gate = MergeSafety()
|
|
948
|
+
return json.dumps(gate.check(base=base, head=head, profile=profile), indent=2, default=str)
|
|
949
|
+
except Exception as exc:
|
|
950
|
+
return json.dumps({"error": str(exc), "base": base, "head": head})
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def main() -> None:
|
|
954
|
+
"""Run the MCP server (stdio)."""
|
|
955
|
+
import sys
|
|
956
|
+
|
|
957
|
+
sys.stderr.write("[SIN-CODE-BUNDLE] MCP server starting (stdio).\n")
|
|
958
|
+
sys.stderr.flush()
|
|
959
|
+
mcp.run()
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
if __name__ == "__main__":
|
|
963
|
+
main()
|