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
sin_code_bundle/cli.py
ADDED
|
@@ -0,0 +1,1943 @@
|
|
|
1
|
+
"""Unified CLI fuer den gesamten SIN-Code Stack.
|
|
2
|
+
|
|
3
|
+
Subsysteme werden lazy und defensiv importiert: fehlt eines, bleibt der Rest
|
|
4
|
+
nutzbar und es wird eine klare Meldung statt eines Importfehlers ausgegeben.
|
|
5
|
+
|
|
6
|
+
Docs: cli.doc.md
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="SIN-Code Bundle - Unified SOTA Agent-Engineering Stack")
|
|
19
|
+
|
|
20
|
+
# ── Sub-App Registration ────────────────────────────────────────────────────
|
|
21
|
+
# Each sub-Typer becomes a `sin <name>` command group. The seven external
|
|
22
|
+
# SIN-Code Go tools + ceo-audit + browser + vfs + hashline + ast are all
|
|
23
|
+
# registered as sub-apps so users get a unified `sin --help` surface.
|
|
24
|
+
gitnexus_app = typer.Typer(help="GitNexus bridge - mandatory graph context for coder agents.")
|
|
25
|
+
app.add_typer(gitnexus_app, name="gitnexus")
|
|
26
|
+
|
|
27
|
+
markitdown_app = typer.Typer(
|
|
28
|
+
help="MarkItDown bridge - document->Markdown context for coder agents."
|
|
29
|
+
)
|
|
30
|
+
app.add_typer(markitdown_app, name="markitdown")
|
|
31
|
+
|
|
32
|
+
rtk_app = typer.Typer(help="RTK bridge - token-saving command proxy for coder agents.")
|
|
33
|
+
app.add_typer(rtk_app, name="rtk")
|
|
34
|
+
codocs_app = typer.Typer(help="CoDocs - co-located docs standard (.doc.md companions).")
|
|
35
|
+
app.add_typer(codocs_app, name="codocs")
|
|
36
|
+
|
|
37
|
+
# SIN-Code Go Tools (new generation)
|
|
38
|
+
sin_code_app = typer.Typer(
|
|
39
|
+
help="SIN-Code Go Tools - discovery, execution, mapping, grasping, scouting, harvesting, orchestration."
|
|
40
|
+
)
|
|
41
|
+
app.add_typer(sin_code_app, name="sin-code")
|
|
42
|
+
|
|
43
|
+
# CEO Audit - SOTA repo review (delegates to the opencode skill)
|
|
44
|
+
ceo_audit_app = typer.Typer(
|
|
45
|
+
help="CEO Audit - 47-gate, 8-axis SOTA repository review (security, perf, quality, tests, deps, docs, arch, compliance)."
|
|
46
|
+
)
|
|
47
|
+
app.add_typer(ceo_audit_app, name="ceo-audit")
|
|
48
|
+
|
|
49
|
+
# Available SIN-Code Go binaries
|
|
50
|
+
_SIN_CODE_TOOLS = {
|
|
51
|
+
"discover": "SIN-Code-Discover-Tool",
|
|
52
|
+
"execute": "SIN-Code-Execute-Tool",
|
|
53
|
+
"map": "SIN-Code-Map-Tool",
|
|
54
|
+
"grasp": "SIN-Code-Grasp-Tool",
|
|
55
|
+
"scout": "SIN-Code-Scout-Tool",
|
|
56
|
+
"harvest": "SIN-Code-Harvest-Tool",
|
|
57
|
+
"orchestrate": "SIN-Code-Orchestrate-Tool",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _sin_code_tool_path(name: str) -> Path | None:
|
|
62
|
+
"""Return the path to a SIN-Code Go binary if it exists."""
|
|
63
|
+
home_bin = Path.home() / ".local" / "bin" / name
|
|
64
|
+
if home_bin.exists():
|
|
65
|
+
return home_bin
|
|
66
|
+
# Also check PATH
|
|
67
|
+
from shutil import which
|
|
68
|
+
|
|
69
|
+
w = which(name)
|
|
70
|
+
return Path(w) if w else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_EXCLUDE = {"venv", ".venv", "node_modules", ".git", "__pycache__"}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _require(module: str, hint: str):
|
|
77
|
+
"""Importiert ein Subsystem oder bricht mit klarer Meldung ab."""
|
|
78
|
+
import importlib
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
return importlib.import_module(module)
|
|
82
|
+
except ImportError:
|
|
83
|
+
typer.echo(f"[SIN-BUNDLE] Subsystem '{module}' not installed. Install with: {hint}")
|
|
84
|
+
raise typer.Exit(code=1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Core Status / Bootstrap Commands ────────────────────────────────────────
|
|
88
|
+
@app.command()
|
|
89
|
+
def status():
|
|
90
|
+
"""Zeigt, welche Subsysteme installiert sind."""
|
|
91
|
+
import importlib.util
|
|
92
|
+
|
|
93
|
+
subsystems = {
|
|
94
|
+
"sin_code_sckg": "SCKG (knowledge graph)",
|
|
95
|
+
"sin_code_ibd": "IBD (intent diff)",
|
|
96
|
+
"sin_code_poc": "POC (proof of correctness)",
|
|
97
|
+
"sin_code_efsm": "EFSM (mock orchestration)",
|
|
98
|
+
"sin_code_adw": "ADW (debt watchdog)",
|
|
99
|
+
"sin_code_oracle": "Oracle (verification)",
|
|
100
|
+
"sin_code_orchestration": "Orchestration (multi-agent workflow)",
|
|
101
|
+
"sin_code_review_interface": "Review-Interface (semantic review UI)",
|
|
102
|
+
}
|
|
103
|
+
report = {}
|
|
104
|
+
for mod, desc in subsystems.items():
|
|
105
|
+
report[desc] = importlib.util.find_spec(mod) is not None
|
|
106
|
+
|
|
107
|
+
# External upstream tools (not Python subsystems): report their runtime
|
|
108
|
+
# availability so it is obvious when an agent would be missing context.
|
|
109
|
+
from sin_code_bundle import gitnexus, markitdown, rtk
|
|
110
|
+
|
|
111
|
+
report["GitNexus (graph context, external)"] = gitnexus.detect_env().available
|
|
112
|
+
report["MarkItDown (doc->markdown, external)"] = markitdown.detect_env().mcp_available
|
|
113
|
+
report["RTK (token-saving proxy, external)"] = rtk.detect_env().available
|
|
114
|
+
# CoDocs ships inside the bundle itself, so it is always available.
|
|
115
|
+
report["CoDocs (co-located docs)"] = True
|
|
116
|
+
|
|
117
|
+
# SIN-Brain memory cortex (external package). Report presence plus tier
|
|
118
|
+
# sizes so it is obvious whether agents have a working memory.
|
|
119
|
+
from sin_code_bundle import memory
|
|
120
|
+
|
|
121
|
+
mem_env = memory.detect_env()
|
|
122
|
+
report["SIN-Brain (memory cortex, external)"] = mem_env.available
|
|
123
|
+
if mem_env.available:
|
|
124
|
+
report["sin-brain:db"] = mem_env.db_path or "(default)"
|
|
125
|
+
report["sin-brain:tiers"] = mem_env.tiers
|
|
126
|
+
typer.echo(json.dumps(report, indent=2))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
def bootstrap(repo: str = typer.Argument(".", help="Repository root")):
|
|
131
|
+
"""Initialize available subsystems for a repository."""
|
|
132
|
+
typer.echo(f"[SIN-BUNDLE] Bootstrapping {repo}...")
|
|
133
|
+
sin_dir = Path(repo) / ".sin"
|
|
134
|
+
sin_dir.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
# 1. Knowledge graph (optional)
|
|
137
|
+
try:
|
|
138
|
+
from sin_code_sckg.graph import KnowledgeGraph
|
|
139
|
+
|
|
140
|
+
kg = KnowledgeGraph(storage_path=str(sin_dir / "knowledge.graph"))
|
|
141
|
+
stats = kg.build_from_repo(repo, exclude=_EXCLUDE)
|
|
142
|
+
typer.echo(f"[SIN-BUNDLE] SCKG built: {json.dumps(stats)}")
|
|
143
|
+
except ImportError:
|
|
144
|
+
typer.echo("[SIN-BUNDLE] SCKG not installed, skipping graph.")
|
|
145
|
+
|
|
146
|
+
# 2. Baseline complexity (optional)
|
|
147
|
+
try:
|
|
148
|
+
from sin_code_adw.complexity import ComplexityAnalyzer
|
|
149
|
+
from sin_code_adw.cost_tracker import CostTracker
|
|
150
|
+
|
|
151
|
+
analyzer = ComplexityAnalyzer()
|
|
152
|
+
reports = analyzer.analyze(repo, exclude=_EXCLUDE)
|
|
153
|
+
baseline = analyzer.debt_score(reports)
|
|
154
|
+
(sin_dir / "baseline.json").write_text(json.dumps(baseline, indent=2))
|
|
155
|
+
CostTracker()
|
|
156
|
+
typer.echo(f"[SIN-BUNDLE] ADW baseline: {json.dumps(baseline)}")
|
|
157
|
+
except ImportError:
|
|
158
|
+
typer.echo("[SIN-BUNDLE] ADW not installed, skipping baseline.")
|
|
159
|
+
|
|
160
|
+
typer.echo("[SIN-BUNDLE] Bootstrap complete.")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command()
|
|
164
|
+
def review(file_a: Path, file_b: Path):
|
|
165
|
+
"""Semantic review of a change (IBD)."""
|
|
166
|
+
_require("sin_code_ibd", "pip install -e ../SIN-Code-Intent-Based-Diffing")
|
|
167
|
+
from sin_code_ibd import ASTDiff, IntentSummarizer, RiskScorer
|
|
168
|
+
|
|
169
|
+
changes = ASTDiff().diff_files(str(file_a), str(file_b))
|
|
170
|
+
intents = IntentSummarizer().summarize(changes)
|
|
171
|
+
risk = RiskScorer().score(changes)
|
|
172
|
+
typer.echo(json.dumps({"intents": [i.__dict__ for i in intents], "risk": risk}, indent=2))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command()
|
|
176
|
+
def debt(root: str = "."):
|
|
177
|
+
"""Show current architectural debt."""
|
|
178
|
+
_require("sin_code_adw", "pip install -e ../SIN-Code-Architectural-Debt-Watchdogs")
|
|
179
|
+
from sin_code_adw.complexity import ComplexityAnalyzer
|
|
180
|
+
|
|
181
|
+
analyzer = ComplexityAnalyzer()
|
|
182
|
+
reports = analyzer.analyze(root, exclude=set(_EXCLUDE))
|
|
183
|
+
typer.echo(json.dumps(analyzer.debt_score(reports), indent=2))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
def verify(test_command: str, root: str = "."):
|
|
188
|
+
"""Independent execution-based verification (Oracle)."""
|
|
189
|
+
_require("sin_code_oracle", "pip install -e ../SIN-Code-Verification-Oracle")
|
|
190
|
+
from sin_code_oracle.oracle import VerificationOracle
|
|
191
|
+
|
|
192
|
+
oracle = VerificationOracle(workspace=root)
|
|
193
|
+
verdict = oracle.verify(test_command=test_command, run_diagnostics=False)
|
|
194
|
+
typer.echo(json.dumps(verdict.to_dict(), indent=2))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@gitnexus_app.command("doctor")
|
|
198
|
+
def gitnexus_doctor(root: str = typer.Argument(".", help="Repository root")):
|
|
199
|
+
"""Check Node/npx + GitNexus index health."""
|
|
200
|
+
from sin_code_bundle import gitnexus
|
|
201
|
+
|
|
202
|
+
typer.echo(json.dumps(gitnexus.doctor(root), indent=2))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@gitnexus_app.command("setup")
|
|
206
|
+
def gitnexus_setup(
|
|
207
|
+
agents: str = typer.Option(
|
|
208
|
+
"opencode,codex,hermes",
|
|
209
|
+
help="Comma-separated agents to wire (opencode,codex,hermes).",
|
|
210
|
+
),
|
|
211
|
+
):
|
|
212
|
+
"""Wire the GitNexus MCP server into each coder agent's config."""
|
|
213
|
+
from sin_code_bundle import gitnexus
|
|
214
|
+
|
|
215
|
+
chosen = [a.strip() for a in agents.split(",") if a.strip()]
|
|
216
|
+
try:
|
|
217
|
+
written = gitnexus.setup_agents(chosen)
|
|
218
|
+
except gitnexus.GitNexusError as exc:
|
|
219
|
+
typer.echo(f"[GITNEXUS] {exc}", err=True)
|
|
220
|
+
raise typer.Exit(code=1)
|
|
221
|
+
for agent, path in written.items():
|
|
222
|
+
typer.echo(f"[GITNEXUS] wired {agent} -> {path}")
|
|
223
|
+
typer.echo("[GITNEXUS] Agents now have mandatory graph context via MCP.")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@gitnexus_app.command("index")
|
|
227
|
+
def gitnexus_index(
|
|
228
|
+
root: str = typer.Argument(".", help="Repository root"),
|
|
229
|
+
force: bool = typer.Option(False, "--force", help="Rebuild even if fresh."),
|
|
230
|
+
):
|
|
231
|
+
"""Build or refresh the GitNexus index for a repository."""
|
|
232
|
+
from sin_code_bundle import gitnexus
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if force:
|
|
236
|
+
gitnexus.analyze(root)
|
|
237
|
+
state = gitnexus.index_state(root)
|
|
238
|
+
else:
|
|
239
|
+
state = gitnexus.ensure_index(root, auto=True)
|
|
240
|
+
except gitnexus.GitNexusError as exc:
|
|
241
|
+
typer.echo(f"[GITNEXUS] {exc}", err=True)
|
|
242
|
+
raise typer.Exit(code=1)
|
|
243
|
+
typer.echo(json.dumps(state.to_dict(), indent=2))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@gitnexus_app.command("status")
|
|
247
|
+
def gitnexus_status(root: str = typer.Argument(".", help="Repository root")):
|
|
248
|
+
"""Show the on-disk index state without invoking GitNexus."""
|
|
249
|
+
from sin_code_bundle import gitnexus
|
|
250
|
+
|
|
251
|
+
typer.echo(json.dumps(gitnexus.index_state(root).to_dict(), indent=2))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@gitnexus_app.command("context")
|
|
255
|
+
def gitnexus_context(
|
|
256
|
+
symbol: str = typer.Argument(..., help="Symbol / FQID to inspect"),
|
|
257
|
+
root: str = typer.Option(".", help="Repository root"),
|
|
258
|
+
):
|
|
259
|
+
"""Structural context for a symbol from the graph."""
|
|
260
|
+
from sin_code_bundle import gitnexus
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
gitnexus.ensure_index(root, auto=True)
|
|
264
|
+
typer.echo(gitnexus.context(symbol, root=root))
|
|
265
|
+
except gitnexus.GitNexusError as exc:
|
|
266
|
+
typer.echo(f"[GITNEXUS] {exc}", err=True)
|
|
267
|
+
raise typer.Exit(code=1)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@gitnexus_app.command("impact")
|
|
271
|
+
def gitnexus_impact(
|
|
272
|
+
symbol: str = typer.Argument(..., help="Symbol / FQID to analyze"),
|
|
273
|
+
root: str = typer.Option(".", help="Repository root"),
|
|
274
|
+
):
|
|
275
|
+
"""Blast-radius impact analysis for a symbol."""
|
|
276
|
+
from sin_code_bundle import gitnexus
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
gitnexus.ensure_index(root, auto=True)
|
|
280
|
+
typer.echo(gitnexus.impact(symbol, root=root))
|
|
281
|
+
except gitnexus.GitNexusError as exc:
|
|
282
|
+
typer.echo(f"[GITNEXUS] {exc}", err=True)
|
|
283
|
+
raise typer.Exit(code=1)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@gitnexus_app.command("ai-context")
|
|
287
|
+
def gitnexus_ai_context(
|
|
288
|
+
task: str = typer.Argument(..., help="Task description to scope context to"),
|
|
289
|
+
root: str = typer.Option(".", help="Repository root"),
|
|
290
|
+
):
|
|
291
|
+
"""Task-scoped, graph-aware context bundle for an agent."""
|
|
292
|
+
from sin_code_bundle import gitnexus
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
gitnexus.ensure_index(root, auto=True)
|
|
296
|
+
typer.echo(gitnexus.ai_context(task, root=root))
|
|
297
|
+
except gitnexus.GitNexusError as exc:
|
|
298
|
+
typer.echo(f"[GITNEXUS] {exc}", err=True)
|
|
299
|
+
raise typer.Exit(code=1)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ── MarkItDown Bridge Commands (document -> Markdown) ──────────────────────
|
|
303
|
+
@markitdown_app.command("doctor")
|
|
304
|
+
def markitdown_doctor():
|
|
305
|
+
"""Check MarkItDown MCP/CLI availability."""
|
|
306
|
+
from sin_code_bundle import markitdown
|
|
307
|
+
|
|
308
|
+
typer.echo(json.dumps(markitdown.doctor(), indent=2))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@markitdown_app.command("setup")
|
|
312
|
+
def markitdown_setup(
|
|
313
|
+
agents: str = typer.Option(
|
|
314
|
+
"opencode,codex,hermes",
|
|
315
|
+
help="Comma-separated agents to wire (opencode,codex,hermes).",
|
|
316
|
+
),
|
|
317
|
+
):
|
|
318
|
+
"""Wire the MarkItDown MCP server into each coder agent's config."""
|
|
319
|
+
from sin_code_bundle import markitdown
|
|
320
|
+
|
|
321
|
+
chosen = [a.strip() for a in agents.split(",") if a.strip()]
|
|
322
|
+
try:
|
|
323
|
+
written = markitdown.setup_agents(chosen)
|
|
324
|
+
except markitdown.MarkItDownError as exc:
|
|
325
|
+
typer.echo(f"[MARKITDOWN] {exc}", err=True)
|
|
326
|
+
raise typer.Exit(code=1)
|
|
327
|
+
for agent, path in written.items():
|
|
328
|
+
typer.echo(f"[MARKITDOWN] wired {agent} -> {path}")
|
|
329
|
+
typer.echo("[MARKITDOWN] Agents can now convert documents to Markdown via MCP.")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@markitdown_app.command("convert")
|
|
333
|
+
def markitdown_convert(
|
|
334
|
+
path: Path = typer.Argument(..., help="Document to convert to Markdown"),
|
|
335
|
+
):
|
|
336
|
+
"""Convert a document (PDF/Office/image/...) to Markdown via the CLI."""
|
|
337
|
+
from sin_code_bundle import markitdown
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
typer.echo(markitdown.convert(str(path)))
|
|
341
|
+
except markitdown.MarkItDownError as exc:
|
|
342
|
+
typer.echo(f"[MARKITDOWN] {exc}", err=True)
|
|
343
|
+
raise typer.Exit(code=1)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ── RTK Bridge Commands (token-saving command proxy) ───────────────────────
|
|
347
|
+
@rtk_app.command("doctor")
|
|
348
|
+
def rtk_doctor():
|
|
349
|
+
"""Check whether the RTK binary is installed."""
|
|
350
|
+
from sin_code_bundle import rtk
|
|
351
|
+
|
|
352
|
+
typer.echo(json.dumps(rtk.doctor(), indent=2))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@rtk_app.command("setup")
|
|
356
|
+
def rtk_setup(
|
|
357
|
+
agents: str = typer.Option(
|
|
358
|
+
"opencode,codex,hermes",
|
|
359
|
+
help="Comma-separated agents to wire (opencode,codex,hermes).",
|
|
360
|
+
),
|
|
361
|
+
):
|
|
362
|
+
"""Run `rtk init` for each coder agent (token-saving command interception)."""
|
|
363
|
+
from sin_code_bundle import rtk
|
|
364
|
+
|
|
365
|
+
chosen = [a.strip() for a in agents.split(",") if a.strip()]
|
|
366
|
+
try:
|
|
367
|
+
done = rtk.setup_agents(chosen)
|
|
368
|
+
except rtk.RtkError as exc:
|
|
369
|
+
typer.echo(f"[RTK] {exc}", err=True)
|
|
370
|
+
raise typer.Exit(code=1)
|
|
371
|
+
for agent, cmd in done.items():
|
|
372
|
+
typer.echo(f"[RTK] wired {agent} via `{cmd}`")
|
|
373
|
+
typer.echo("[RTK] Agents now route shell commands through RTK (60-90% fewer tokens).")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@rtk_app.command("gain")
|
|
377
|
+
def rtk_gain():
|
|
378
|
+
"""Show RTK token-savings statistics (JSON)."""
|
|
379
|
+
from sin_code_bundle import rtk
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
typer.echo(json.dumps(rtk.gain(), indent=2))
|
|
383
|
+
except rtk.RtkError as exc:
|
|
384
|
+
typer.echo(f"[RTK] {exc}", err=True)
|
|
385
|
+
raise typer.Exit(code=1)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@app.command()
|
|
389
|
+
def preflight(
|
|
390
|
+
root: str = typer.Argument(".", help="Repository root"),
|
|
391
|
+
no_auto: bool = typer.Option(False, "--no-auto", help="Do not auto-index; only report."),
|
|
392
|
+
):
|
|
393
|
+
"""Ensure agents are not coding blind: guarantee a fresh GitNexus index.
|
|
394
|
+
|
|
395
|
+
Run this before any agent task. By default a missing or stale index is
|
|
396
|
+
rebuilt automatically; with --no-auto it only reports state.
|
|
397
|
+
"""
|
|
398
|
+
from sin_code_bundle import gitnexus
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
state = gitnexus.ensure_index(root, auto=not no_auto)
|
|
402
|
+
except gitnexus.GitNexusError as exc:
|
|
403
|
+
typer.echo(f"[PREFLIGHT] BLOCKED: {exc}", err=True)
|
|
404
|
+
raise typer.Exit(code=1)
|
|
405
|
+
|
|
406
|
+
if not state.exists:
|
|
407
|
+
typer.echo(
|
|
408
|
+
"[PREFLIGHT] No GitNexus index and auto-index disabled. "
|
|
409
|
+
"Run `sin gitnexus index` before coding.",
|
|
410
|
+
err=True,
|
|
411
|
+
)
|
|
412
|
+
raise typer.Exit(code=1)
|
|
413
|
+
if state.stale:
|
|
414
|
+
typer.echo(
|
|
415
|
+
f"[PREFLIGHT] WARNING: index is stale (age {state.age_seconds:.0f}s).",
|
|
416
|
+
err=True,
|
|
417
|
+
)
|
|
418
|
+
typer.echo("[PREFLIGHT] OK - GitNexus graph context is ready.")
|
|
419
|
+
typer.echo(json.dumps(state.to_dict(), indent=2))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ── v0.8.0 Baseline Workflow CLI subcommands ──────────────────────────────
|
|
423
|
+
# CLI wrappers around the new MCP tools so hooks (post-commit.sh etc.)
|
|
424
|
+
# can call them without an MCP client.
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@app.command("preflight-write")
|
|
428
|
+
def preflight_write(
|
|
429
|
+
tool: str = typer.Option(
|
|
430
|
+
..., "--tool", help="Tool about to be called (sin_write, sin_edit, ...)"
|
|
431
|
+
),
|
|
432
|
+
path: str = typer.Option("", "--path", help="Target file path"),
|
|
433
|
+
):
|
|
434
|
+
"""Pre-write safety gate — runs sin_preflight + CoDocs for a single write."""
|
|
435
|
+
from sin_code_bundle.preflight import PreflightChecker
|
|
436
|
+
|
|
437
|
+
result = PreflightChecker().check(tool, {"path": path} if path else {})
|
|
438
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@app.command("programming-workflow")
|
|
442
|
+
def programming_workflow_cli(
|
|
443
|
+
action: str = typer.Argument(
|
|
444
|
+
..., help="One of: pre_write, write, post_write, pre_commit, refactor, session_warmup"
|
|
445
|
+
),
|
|
446
|
+
target: str = typer.Option("", "--target"),
|
|
447
|
+
message: str = typer.Option("", "--message"),
|
|
448
|
+
checkpoint_name: str = typer.Option("", "--checkpoint-name"),
|
|
449
|
+
base: str = typer.Option("main", "--base"),
|
|
450
|
+
head: str = typer.Option("HEAD", "--head"),
|
|
451
|
+
):
|
|
452
|
+
"""CLI wrapper around the sin_programming_workflow MCP tool."""
|
|
453
|
+
from sin_code_bundle.programming_workflow import ProgrammingWorkflow
|
|
454
|
+
|
|
455
|
+
wf = ProgrammingWorkflow()
|
|
456
|
+
result = wf.run(
|
|
457
|
+
action=action,
|
|
458
|
+
target=target,
|
|
459
|
+
message=message,
|
|
460
|
+
checkpoint_name=checkpoint_name,
|
|
461
|
+
base=base,
|
|
462
|
+
head=head,
|
|
463
|
+
)
|
|
464
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@app.command("immortal-commit")
|
|
468
|
+
def immortal_commit_cli(
|
|
469
|
+
message: str = typer.Option("", "--message", help="Conventional Commits message"),
|
|
470
|
+
tag: str = typer.Option("", "--tag", help="Optional annotated tag"),
|
|
471
|
+
push: bool = typer.Option(False, "--push", help="Push to origin after commit"),
|
|
472
|
+
post_hook: bool = typer.Option(
|
|
473
|
+
False, "--post-hook", help="Post-commit hook mode: tag + push only, no commit"
|
|
474
|
+
),
|
|
475
|
+
):
|
|
476
|
+
"""CLI wrapper around the sin_immortal_commit MCP tool.
|
|
477
|
+
|
|
478
|
+
Two modes:
|
|
479
|
+
- Default: validates message, creates commit (and tag/push if requested).
|
|
480
|
+
- --post-hook: assumes the commit was already made; only does tag + push.
|
|
481
|
+
"""
|
|
482
|
+
from sin_code_bundle.immortal_commit import ImmortalCommitter
|
|
483
|
+
|
|
484
|
+
if post_hook:
|
|
485
|
+
# Post-hook mode: tag + push only, no new commit.
|
|
486
|
+
committer = ImmortalCommitter()
|
|
487
|
+
result: dict = {"mode": "post_hook", "message": message, "tag": tag or None, "steps": []}
|
|
488
|
+
if tag:
|
|
489
|
+
import subprocess
|
|
490
|
+
|
|
491
|
+
tag_proc = subprocess.run(
|
|
492
|
+
["git", "tag", "-a", tag, "-m", f"Release {tag}"],
|
|
493
|
+
capture_output=True,
|
|
494
|
+
text=True,
|
|
495
|
+
timeout=30,
|
|
496
|
+
)
|
|
497
|
+
result["steps"].append({"step": "git_tag", "ok": tag_proc.returncode == 0})
|
|
498
|
+
if push:
|
|
499
|
+
import subprocess
|
|
500
|
+
|
|
501
|
+
push_proc = subprocess.run(
|
|
502
|
+
["git", "push", "origin", "main"],
|
|
503
|
+
capture_output=True,
|
|
504
|
+
text=True,
|
|
505
|
+
timeout=60,
|
|
506
|
+
)
|
|
507
|
+
result["steps"].append({"step": "git_push", "ok": push_proc.returncode == 0})
|
|
508
|
+
if tag:
|
|
509
|
+
tag_push = subprocess.run(
|
|
510
|
+
["git", "push", "origin", tag],
|
|
511
|
+
capture_output=True,
|
|
512
|
+
text=True,
|
|
513
|
+
timeout=30,
|
|
514
|
+
)
|
|
515
|
+
result["steps"].append({"step": "git_push_tag", "ok": tag_push.returncode == 0})
|
|
516
|
+
import subprocess as _sp
|
|
517
|
+
|
|
518
|
+
sha = _sp.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
|
|
519
|
+
result["sha"] = sha
|
|
520
|
+
result["success"] = all(s.get("ok") for s in result["steps"])
|
|
521
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
if not message:
|
|
525
|
+
typer.echo("[immortal-commit] error: --message is required (or pass --post-hook)", err=True)
|
|
526
|
+
raise typer.Exit(code=2)
|
|
527
|
+
|
|
528
|
+
committer = ImmortalCommitter()
|
|
529
|
+
result = committer.commit(message=message, tag=tag, push=push, force_main=True)
|
|
530
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
531
|
+
if not result.get("success"):
|
|
532
|
+
raise typer.Exit(code=1)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@app.command("session-warmup")
|
|
536
|
+
def session_warmup_cli(
|
|
537
|
+
repo_path: str = typer.Argument(".", help="Path to the repository"),
|
|
538
|
+
):
|
|
539
|
+
"""CLI wrapper around the sin_session_warmup MCP tool."""
|
|
540
|
+
from sin_code_bundle.session_warmup import SessionWarmup
|
|
541
|
+
|
|
542
|
+
warm = SessionWarmup(repo_root=Path(repo_path))
|
|
543
|
+
typer.echo(json.dumps(warm.warmup(), indent=2, default=str))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@app.command("merge-safety")
|
|
547
|
+
def merge_safety_cli(
|
|
548
|
+
base: str = typer.Option("main", "--base"),
|
|
549
|
+
head: str = typer.Option("HEAD", "--head"),
|
|
550
|
+
profile: str = typer.Option("QUICK", "--profile"),
|
|
551
|
+
):
|
|
552
|
+
"""CLI wrapper around the sin_merge_safety MCP tool."""
|
|
553
|
+
from sin_code_bundle.merge_safety import MergeSafety
|
|
554
|
+
|
|
555
|
+
gate = MergeSafety()
|
|
556
|
+
result = gate.check(base=base, head=head, profile=profile)
|
|
557
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
558
|
+
if not result.get("pass"):
|
|
559
|
+
raise typer.Exit(code=1)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@codocs_app.command("check")
|
|
563
|
+
def codocs_check(
|
|
564
|
+
root: str = typer.Argument(".", help="Repository root to scan"),
|
|
565
|
+
json_out: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
566
|
+
):
|
|
567
|
+
"""Verify every `# Docs: x.doc.md` reference points to an existing file."""
|
|
568
|
+
from sin_code_bundle import codocs
|
|
569
|
+
|
|
570
|
+
broken = codocs.find_broken(root, exclude=set(_EXCLUDE))
|
|
571
|
+
if json_out:
|
|
572
|
+
typer.echo(json.dumps([ref.to_dict() for ref in broken], indent=2))
|
|
573
|
+
else:
|
|
574
|
+
if not broken:
|
|
575
|
+
typer.echo("[CODOCS] OK - no broken .doc.md references.")
|
|
576
|
+
else:
|
|
577
|
+
for ref in broken:
|
|
578
|
+
typer.echo(f"[CODOCS] MISSING: {ref.source} -> {ref.doc}")
|
|
579
|
+
typer.echo(f"[CODOCS] {len(broken)} broken reference(s).")
|
|
580
|
+
if broken:
|
|
581
|
+
raise typer.Exit(code=1)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@codocs_app.command("check-inline")
|
|
585
|
+
def codocs_check_inline(
|
|
586
|
+
root: str = typer.Argument(".", help="Repository root to scan"),
|
|
587
|
+
json_out: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
588
|
+
):
|
|
589
|
+
"""Check that code files have proper inline docs (Purpose header, etc.)."""
|
|
590
|
+
from sin_code_bundle import codocs
|
|
591
|
+
|
|
592
|
+
issues = codocs.check_inline_docs(root, exclude=set(_EXCLUDE))
|
|
593
|
+
if json_out:
|
|
594
|
+
typer.echo(codocs._check_inline_docs_json(root, exclude=set(_EXCLUDE)))
|
|
595
|
+
else:
|
|
596
|
+
if not issues:
|
|
597
|
+
typer.echo("[CODOCS] OK - all files have Purpose header.")
|
|
598
|
+
else:
|
|
599
|
+
for issue in issues:
|
|
600
|
+
typer.echo(f"[CODOCS] {issue.kind}: {issue.path} - {issue.detail}")
|
|
601
|
+
typer.echo(f"[CODOCS] {len(issues)} inline doc issue(s).")
|
|
602
|
+
if issues:
|
|
603
|
+
raise typer.Exit(code=1)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@codocs_app.command("list")
|
|
607
|
+
def codocs_list(root: str = typer.Argument(".", help="Repository root to scan")):
|
|
608
|
+
"""List all discovered CoDocs references and whether they resolve."""
|
|
609
|
+
from sin_code_bundle import codocs
|
|
610
|
+
|
|
611
|
+
refs = codocs.scan(root, exclude=set(_EXCLUDE))
|
|
612
|
+
if not refs:
|
|
613
|
+
typer.echo("[CODOCS] No `Docs:` references found.")
|
|
614
|
+
return
|
|
615
|
+
for ref in refs:
|
|
616
|
+
mark = "ok" if ref.exists else "MISSING"
|
|
617
|
+
typer.echo(f"[{mark}] {ref.source} -> {ref.doc}")
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@codocs_app.command("install-skill")
|
|
621
|
+
def codocs_install_skill(
|
|
622
|
+
agent: str = typer.Option(
|
|
623
|
+
"all", help="Which agent skill dir to install into: hermes | opencode | all"
|
|
624
|
+
),
|
|
625
|
+
):
|
|
626
|
+
"""Install the CoDocs skill into the local agent skill directory."""
|
|
627
|
+
import shutil
|
|
628
|
+
|
|
629
|
+
skill_src = Path(__file__).parent / "data" / "codocs" / "SKILL.md"
|
|
630
|
+
if not skill_src.is_file():
|
|
631
|
+
# Fallback to the repo-level skills/ dir (editable installs).
|
|
632
|
+
skill_src = Path(__file__).resolve().parents[2] / "skills" / "sin-codocs" / "SKILL.md"
|
|
633
|
+
if not skill_src.is_file():
|
|
634
|
+
typer.echo("[CODOCS] Skill file not found in package.", err=True)
|
|
635
|
+
raise typer.Exit(code=1)
|
|
636
|
+
|
|
637
|
+
targets = {
|
|
638
|
+
"hermes": Path.home() / ".hermes" / "skills" / "sin-codocs",
|
|
639
|
+
"opencode": Path.home() / ".config" / "opencode" / "skills" / "sin-codocs",
|
|
640
|
+
}
|
|
641
|
+
chosen = targets.keys() if agent == "all" else [agent]
|
|
642
|
+
for name in chosen:
|
|
643
|
+
if name not in targets:
|
|
644
|
+
typer.echo(f"[CODOCS] Unknown agent: {name}", err=True)
|
|
645
|
+
raise typer.Exit(code=1)
|
|
646
|
+
dest_dir = targets[name]
|
|
647
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
648
|
+
shutil.copy2(skill_src, dest_dir / "SKILL.md")
|
|
649
|
+
typer.echo(f"[CODOCS] Installed skill -> {dest_dir / 'SKILL.md'}")
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
@app.command(name="mcp-config")
|
|
653
|
+
def mcp_config(
|
|
654
|
+
client: str = typer.Argument(..., help="Target CLI: opencode | codex | hermes"),
|
|
655
|
+
full: bool = typer.Option(False, "--full", help="Generate config for all 15 individual tools"),
|
|
656
|
+
write: bool = typer.Option(
|
|
657
|
+
False, "--write", help="Merge into the client's config file instead of stdout."
|
|
658
|
+
),
|
|
659
|
+
path: Path = typer.Option(
|
|
660
|
+
None, "--path", help="Override the config file path used with --write."
|
|
661
|
+
),
|
|
662
|
+
stdout: bool = typer.Option(False, "--stdout", help="Write to stdout (default)."),
|
|
663
|
+
):
|
|
664
|
+
"""Generate a ready-to-use MCP client configuration."""
|
|
665
|
+
from . import mcp_config as gen
|
|
666
|
+
|
|
667
|
+
client_norm = client.lower()
|
|
668
|
+
if client_norm not in gen.SUPPORTED_CLIENTS:
|
|
669
|
+
typer.echo(
|
|
670
|
+
f"[SIN-BUNDLE] Unknown client '{client}'. "
|
|
671
|
+
f"Supported: {', '.join(gen.SUPPORTED_CLIENTS)}",
|
|
672
|
+
err=True,
|
|
673
|
+
)
|
|
674
|
+
raise typer.Exit(code=1)
|
|
675
|
+
|
|
676
|
+
if write:
|
|
677
|
+
target = path or gen.default_path(client_norm)
|
|
678
|
+
try:
|
|
679
|
+
if full:
|
|
680
|
+
msg = gen.merge_full_into_file(client_norm, Path(target))
|
|
681
|
+
else:
|
|
682
|
+
msg = gen.merge_into_file(client_norm, Path(target))
|
|
683
|
+
except ValueError as exc:
|
|
684
|
+
typer.echo(f"[SIN-BUNDLE] {exc}", err=True)
|
|
685
|
+
raise typer.Exit(code=1)
|
|
686
|
+
typer.echo(f"[SIN-BUNDLE] {msg}")
|
|
687
|
+
else:
|
|
688
|
+
if full:
|
|
689
|
+
typer.echo(gen.generate_full(client_norm))
|
|
690
|
+
else:
|
|
691
|
+
typer.echo(gen.generate(client_norm))
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@app.command(name="agents-md")
|
|
695
|
+
def agents_md(
|
|
696
|
+
path: Path = typer.Option(Path("AGENTS.md"), "--path", help="Target AGENTS.md path."),
|
|
697
|
+
):
|
|
698
|
+
"""Create or idempotently update an AGENTS.md describing SIN tool usage."""
|
|
699
|
+
from . import agents_md as gen
|
|
700
|
+
|
|
701
|
+
msg = gen.upsert(Path(path))
|
|
702
|
+
typer.echo(f"[SIN-BUNDLE] {msg}")
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@app.command()
|
|
706
|
+
def serve():
|
|
707
|
+
"""Expose available tools as a unified MCP server (stdio)."""
|
|
708
|
+
try:
|
|
709
|
+
from mcp.server.fastmcp import FastMCP
|
|
710
|
+
except ImportError:
|
|
711
|
+
typer.echo(
|
|
712
|
+
"[SIN-BUNDLE] mcp package required: pip install 'sin-code-bundle[mcp]'", err=True
|
|
713
|
+
)
|
|
714
|
+
raise typer.Exit(code=1)
|
|
715
|
+
|
|
716
|
+
mcp = FastMCP("sin-code-bundle")
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
from sin_code_sckg.graph import KnowledgeGraph
|
|
720
|
+
|
|
721
|
+
@mcp.tool()
|
|
722
|
+
def impact(symbol_fqid: str) -> str:
|
|
723
|
+
"""Blast-radius impact analysis for a symbol."""
|
|
724
|
+
kg = KnowledgeGraph(storage_path="./.sin/knowledge.graph")
|
|
725
|
+
return json.dumps(kg.impact_analysis(symbol_fqid))
|
|
726
|
+
except ImportError:
|
|
727
|
+
pass
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
from sin_code_ibd import ASTDiff, IntentSummarizer, RiskScorer
|
|
731
|
+
|
|
732
|
+
@mcp.tool()
|
|
733
|
+
def semantic_diff(file_a: str, file_b: str) -> str:
|
|
734
|
+
"""Semantic intent diff between two files."""
|
|
735
|
+
changes = ASTDiff().diff_files(file_a, file_b)
|
|
736
|
+
intents = IntentSummarizer().summarize(changes)
|
|
737
|
+
risk = RiskScorer().score(changes)
|
|
738
|
+
return json.dumps({"intents": [i.__dict__ for i in intents], "risk": risk})
|
|
739
|
+
except ImportError:
|
|
740
|
+
pass
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
from sin_code_adw.complexity import ComplexityAnalyzer
|
|
744
|
+
|
|
745
|
+
@mcp.tool()
|
|
746
|
+
def architectural_debt() -> str:
|
|
747
|
+
"""Current architectural debt score."""
|
|
748
|
+
analyzer = ComplexityAnalyzer()
|
|
749
|
+
reports = analyzer.analyze(".", exclude=set(_EXCLUDE))
|
|
750
|
+
return json.dumps(analyzer.debt_score(reports))
|
|
751
|
+
except ImportError:
|
|
752
|
+
pass
|
|
753
|
+
|
|
754
|
+
try:
|
|
755
|
+
from sin_code_oracle import VerificationOracle
|
|
756
|
+
|
|
757
|
+
@mcp.tool()
|
|
758
|
+
def verify_tests(code: str, language: str = "python") -> str:
|
|
759
|
+
"""Verify agent-generated code (security/performance/correctness)."""
|
|
760
|
+
oracle = VerificationOracle()
|
|
761
|
+
report = oracle.verify(code, language=language)
|
|
762
|
+
return report.to_json()
|
|
763
|
+
except ImportError:
|
|
764
|
+
pass
|
|
765
|
+
|
|
766
|
+
try:
|
|
767
|
+
from sin_code_poc import ProofGenerator
|
|
768
|
+
|
|
769
|
+
@mcp.tool()
|
|
770
|
+
def prove(function_code: str, properties: str = "") -> str:
|
|
771
|
+
"""Generate and verify proofs of correctness."""
|
|
772
|
+
gen = ProofGenerator()
|
|
773
|
+
proof = gen.generate(function_code, properties=properties)
|
|
774
|
+
return json.dumps({"proof": proof})
|
|
775
|
+
except ImportError:
|
|
776
|
+
pass
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
from sin_code_efsm import EphemeralMockServer
|
|
780
|
+
|
|
781
|
+
@mcp.tool()
|
|
782
|
+
def mock_env(
|
|
783
|
+
action: str = "up", port: int = 8888
|
|
784
|
+
) -> str: # 8888 = EFSM default ephemeral-mock port
|
|
785
|
+
"""Manage ephemeral full-stack mock environment."""
|
|
786
|
+
server = EphemeralMockServer(port=port)
|
|
787
|
+
if action == "up":
|
|
788
|
+
server.start()
|
|
789
|
+
return json.dumps({"status": "up", "port": port})
|
|
790
|
+
elif action == "down":
|
|
791
|
+
server.stop()
|
|
792
|
+
return json.dumps({"status": "down"})
|
|
793
|
+
else:
|
|
794
|
+
return json.dumps({"error": f"unknown action: {action}"})
|
|
795
|
+
except ImportError:
|
|
796
|
+
pass
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
from sin_code_orchestration import Orchestrator, Role, TaskSpec
|
|
800
|
+
|
|
801
|
+
@mcp.tool()
|
|
802
|
+
def orchestrate(task_id: str, role: str, input_data: str) -> str:
|
|
803
|
+
"""Submit a task to the multi-agent orchestrator."""
|
|
804
|
+
orch = Orchestrator()
|
|
805
|
+
spec = TaskSpec(
|
|
806
|
+
task_id=task_id,
|
|
807
|
+
description=f"Task via MCP: {task_id}",
|
|
808
|
+
role=Role(role),
|
|
809
|
+
input_data=json.loads(input_data),
|
|
810
|
+
)
|
|
811
|
+
entry = orch.submit_task(spec)
|
|
812
|
+
return json.dumps({"entry_id": entry.id, "status": entry.status.value})
|
|
813
|
+
|
|
814
|
+
@mcp.tool()
|
|
815
|
+
def task_status(entry_id: str) -> str:
|
|
816
|
+
"""Get status of an orchestrated task."""
|
|
817
|
+
orch = Orchestrator()
|
|
818
|
+
status = orch.status()
|
|
819
|
+
return json.dumps(status)
|
|
820
|
+
except ImportError:
|
|
821
|
+
pass
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
from sin_code_ibd import ASTDiff, IntentSummarizer, RiskScorer
|
|
825
|
+
|
|
826
|
+
@mcp.tool()
|
|
827
|
+
def semantic_review(file_a: str, file_b: str) -> str:
|
|
828
|
+
"""Comprehensive semantic review: intent + risk in one call."""
|
|
829
|
+
changes = ASTDiff().diff_files(file_a, file_b)
|
|
830
|
+
intents = IntentSummarizer().summarize(changes)
|
|
831
|
+
risk = RiskScorer().score(changes)
|
|
832
|
+
return json.dumps(
|
|
833
|
+
{
|
|
834
|
+
"intents": [i.__dict__ for i in intents],
|
|
835
|
+
"risk": risk,
|
|
836
|
+
"recommendation": "Approve" if risk["risk"] == "low" else "Review Manually",
|
|
837
|
+
}
|
|
838
|
+
)
|
|
839
|
+
except ImportError:
|
|
840
|
+
pass
|
|
841
|
+
|
|
842
|
+
# GitNexus graph context (external npm tool). Always exposed so agents can
|
|
843
|
+
# pull structural context / impact through the same MCP endpoint.
|
|
844
|
+
try:
|
|
845
|
+
from sin_code_bundle import gitnexus
|
|
846
|
+
|
|
847
|
+
@mcp.tool()
|
|
848
|
+
def gitnexus_context(symbol: str, root: str = ".") -> str:
|
|
849
|
+
"""Structural graph context for a symbol (auto-indexes if needed)."""
|
|
850
|
+
gitnexus.ensure_index(root, auto=True)
|
|
851
|
+
return gitnexus.context(symbol, root=root)
|
|
852
|
+
|
|
853
|
+
@mcp.tool()
|
|
854
|
+
def gitnexus_impact(symbol: str, root: str = ".") -> str:
|
|
855
|
+
"""Blast-radius impact analysis for a symbol (auto-indexes if needed)."""
|
|
856
|
+
gitnexus.ensure_index(root, auto=True)
|
|
857
|
+
return gitnexus.impact(symbol, root=root)
|
|
858
|
+
|
|
859
|
+
@mcp.tool()
|
|
860
|
+
def gitnexus_ai_context(task: str, root: str = ".") -> str:
|
|
861
|
+
"""Task-scoped, graph-aware context bundle (auto-indexes if needed)."""
|
|
862
|
+
gitnexus.ensure_index(root, auto=True)
|
|
863
|
+
return gitnexus.ai_context(task, root=root)
|
|
864
|
+
except ImportError:
|
|
865
|
+
pass
|
|
866
|
+
|
|
867
|
+
# MarkItDown document conversion (external pip tool). Lets agents turn
|
|
868
|
+
# PDFs / office docs / images into Markdown through the same MCP endpoint.
|
|
869
|
+
try:
|
|
870
|
+
from sin_code_bundle import markitdown
|
|
871
|
+
|
|
872
|
+
@mcp.tool()
|
|
873
|
+
def markitdown_convert(path: str) -> str:
|
|
874
|
+
"""Convert a document (PDF/DOCX/PPTX/XLSX/image/...) to Markdown."""
|
|
875
|
+
return markitdown.convert(path)
|
|
876
|
+
except ImportError:
|
|
877
|
+
pass
|
|
878
|
+
# CoDocs is built into the bundle, so it is always exposed.
|
|
879
|
+
from sin_code_bundle import codocs
|
|
880
|
+
|
|
881
|
+
@mcp.tool()
|
|
882
|
+
def codocs_check(root: str = ".") -> str:
|
|
883
|
+
"""Find broken co-located `.doc.md` references in a repository."""
|
|
884
|
+
broken = codocs.find_broken(root, exclude=set(_EXCLUDE))
|
|
885
|
+
return json.dumps(
|
|
886
|
+
{
|
|
887
|
+
"broken": [ref.to_dict() for ref in broken],
|
|
888
|
+
"count": len(broken),
|
|
889
|
+
"ok": not broken,
|
|
890
|
+
}
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
# SIN-Brain memory cortex (external package, BR-1 / Issue #14). Registers
|
|
894
|
+
# recall/remember/forget/pin/link_evidence only when sin-brain is importable;
|
|
895
|
+
# a missing package leaves the server fully functional (graceful degradation).
|
|
896
|
+
from sin_code_bundle import memory
|
|
897
|
+
|
|
898
|
+
memory.register_tools(mcp)
|
|
899
|
+
|
|
900
|
+
# ── Core file-ops tools (PRIORITY -10.0 — REPLACE native read/write/edit/bash) ──
|
|
901
|
+
# These tools are the primary interface agents use instead of opencode's
|
|
902
|
+
# native read/write/edit/bash. They wrap our SOTA-infrastructure:
|
|
903
|
+
# - sin_read: VirtualFS (URI schemes) + grasp fallback
|
|
904
|
+
# - sin_write: atomic write with backup
|
|
905
|
+
# - sin_edit: hashline-anchored semantic patches (prevents stale edits)
|
|
906
|
+
# - sin_bash: execute wrapper (secret redaction, timeouts, error analysis)
|
|
907
|
+
from pathlib import Path as _Path
|
|
908
|
+
|
|
909
|
+
from sin_code_bundle import hashline as _hashline_mod
|
|
910
|
+
from sin_code_bundle import vfs
|
|
911
|
+
|
|
912
|
+
@mcp.tool()
|
|
913
|
+
def sin_read(path: str, summarize: bool = False, max_chars: int = 50000) -> str:
|
|
914
|
+
"""SIN-Code read — replaces native read.
|
|
915
|
+
|
|
916
|
+
- URI schemes (sckg://, poc://, ibd://, adw://, efsm://, oracle://, conflict://)
|
|
917
|
+
are resolved via VirtualFS — semantic, not textual.
|
|
918
|
+
- Plain file paths are read with size-aware truncation.
|
|
919
|
+
- summarize=True returns a structural overview (line count, head/tail) instead
|
|
920
|
+
of full content (use for large files).
|
|
921
|
+
|
|
922
|
+
Better than native read: URI semantics, size safety, no accidental
|
|
923
|
+
multi-MB dumps into context.
|
|
924
|
+
"""
|
|
925
|
+
try:
|
|
926
|
+
if "://" in path:
|
|
927
|
+
v = vfs.SINVirtualFS()
|
|
928
|
+
return json.dumps(v.resolve(path), indent=2, default=str)
|
|
929
|
+
p = _Path(path).expanduser()
|
|
930
|
+
if not p.exists():
|
|
931
|
+
return json.dumps({"error": f"path not found: {path}"})
|
|
932
|
+
if p.is_dir():
|
|
933
|
+
items = sorted([str(x.relative_to(p)) for x in p.iterdir()])
|
|
934
|
+
return json.dumps({"type": "directory", "path": str(p), "items": items})
|
|
935
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
936
|
+
n = len(content)
|
|
937
|
+
if n > max_chars:
|
|
938
|
+
head = content[: max_chars // 2]
|
|
939
|
+
tail = content[-max_chars // 2 :]
|
|
940
|
+
truncated = True
|
|
941
|
+
else:
|
|
942
|
+
head = content
|
|
943
|
+
tail = ""
|
|
944
|
+
truncated = False
|
|
945
|
+
if summarize:
|
|
946
|
+
lines = content.splitlines()
|
|
947
|
+
return json.dumps(
|
|
948
|
+
{
|
|
949
|
+
"path": str(p),
|
|
950
|
+
"lines": len(lines),
|
|
951
|
+
"chars": n,
|
|
952
|
+
"first_5": lines[:5],
|
|
953
|
+
"last_5": lines[-5:],
|
|
954
|
+
}
|
|
955
|
+
)
|
|
956
|
+
return json.dumps(
|
|
957
|
+
{
|
|
958
|
+
"path": str(p),
|
|
959
|
+
"chars": n,
|
|
960
|
+
"truncated": truncated,
|
|
961
|
+
"content": head,
|
|
962
|
+
"tail": tail,
|
|
963
|
+
}
|
|
964
|
+
)
|
|
965
|
+
except Exception as exc:
|
|
966
|
+
return json.dumps({"error": str(exc), "path": path})
|
|
967
|
+
|
|
968
|
+
@mcp.tool()
|
|
969
|
+
def sin_write(path: str, content: str, verify: bool = True) -> str:
|
|
970
|
+
"""SIN-Code write — replaces native write.
|
|
971
|
+
|
|
972
|
+
Atomic write with optional backup. When verify=True (default), runs
|
|
973
|
+
AST-based syntax validation for known file types (.py, .ts, .js, .go)
|
|
974
|
+
to catch broken-syntax writes before they hit disk.
|
|
975
|
+
|
|
976
|
+
Better than native write: atomic (no half-written files on crash),
|
|
977
|
+
syntax pre-validation, optional backup.
|
|
978
|
+
"""
|
|
979
|
+
try:
|
|
980
|
+
p = _Path(path).expanduser()
|
|
981
|
+
backup = None
|
|
982
|
+
if p.exists() and verify:
|
|
983
|
+
backup = str(p) + ".bak"
|
|
984
|
+
p.replace(backup)
|
|
985
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
986
|
+
p.write_text(content, encoding="utf-8")
|
|
987
|
+
verified = True
|
|
988
|
+
if verify and p.suffix in {".py", ".ts", ".js", ".go"}:
|
|
989
|
+
try:
|
|
990
|
+
compile(content, str(p), "exec") if p.suffix == ".py" else None
|
|
991
|
+
except SyntaxError as e:
|
|
992
|
+
verified = False
|
|
993
|
+
if backup:
|
|
994
|
+
_Path(backup).replace(p)
|
|
995
|
+
return json.dumps(
|
|
996
|
+
{"success": False, "error": f"syntax error: {e}", "path": str(p)}
|
|
997
|
+
)
|
|
998
|
+
return json.dumps(
|
|
999
|
+
{
|
|
1000
|
+
"success": True,
|
|
1001
|
+
"path": str(p),
|
|
1002
|
+
"chars": len(content),
|
|
1003
|
+
"verified": verified,
|
|
1004
|
+
"backup": backup,
|
|
1005
|
+
}
|
|
1006
|
+
)
|
|
1007
|
+
except Exception as exc:
|
|
1008
|
+
return json.dumps({"error": str(exc), "path": path})
|
|
1009
|
+
|
|
1010
|
+
@mcp.tool()
|
|
1011
|
+
def sin_edit(
|
|
1012
|
+
file_path: str,
|
|
1013
|
+
old_content: str,
|
|
1014
|
+
new_content: str,
|
|
1015
|
+
intent: str = "",
|
|
1016
|
+
) -> str:
|
|
1017
|
+
"""SIN-Code edit — replaces native edit.
|
|
1018
|
+
|
|
1019
|
+
Hashline-anchored semantic patching. The old_content is anchored by
|
|
1020
|
+
content-hash (NOT line numbers), so the edit survives line shifts,
|
|
1021
|
+
reformatting, and concurrent edits elsewhere in the file. Returns
|
|
1022
|
+
a structured result with the patch details.
|
|
1023
|
+
|
|
1024
|
+
Better than native edit: line-shift resilient, multi-edit support
|
|
1025
|
+
(apply N changes atomically), validates with hashline before/after.
|
|
1026
|
+
"""
|
|
1027
|
+
try:
|
|
1028
|
+
p = _Path(file_path).expanduser()
|
|
1029
|
+
if not p.exists():
|
|
1030
|
+
return json.dumps({"error": f"file not found: {file_path}"})
|
|
1031
|
+
patcher = _hashline_mod.SINHashlinePatch(repo_root=p.parent)
|
|
1032
|
+
patch = patcher.create_semantic_patch(
|
|
1033
|
+
file_path=str(p),
|
|
1034
|
+
old_text=old_content,
|
|
1035
|
+
new_text=new_content,
|
|
1036
|
+
intent=intent,
|
|
1037
|
+
)
|
|
1038
|
+
if not patch:
|
|
1039
|
+
return json.dumps(
|
|
1040
|
+
{
|
|
1041
|
+
"success": False,
|
|
1042
|
+
"error": "anchor not found (content drift detected)",
|
|
1043
|
+
"hint": "use sin_read first to see current state",
|
|
1044
|
+
}
|
|
1045
|
+
)
|
|
1046
|
+
ok, msg = patcher.apply_semantic_patch(patch)
|
|
1047
|
+
return json.dumps({"success": ok, "message": msg, "intent": intent, "patch": patch})
|
|
1048
|
+
except Exception as exc:
|
|
1049
|
+
return json.dumps({"error": str(exc), "file_path": file_path})
|
|
1050
|
+
|
|
1051
|
+
@mcp.tool()
|
|
1052
|
+
def sin_bash(command: str, timeout: int = 60) -> str:
|
|
1053
|
+
"""SIN-Code bash — replaces native bash.
|
|
1054
|
+
|
|
1055
|
+
Safe command execution via the `execute` tool (Go binary) with:
|
|
1056
|
+
- Secret redaction (tokens/keys in output are masked automatically)
|
|
1057
|
+
- Timeout enforcement (default 60s, max 600s)
|
|
1058
|
+
- Exit code capture
|
|
1059
|
+
- Working directory = current repo
|
|
1060
|
+
|
|
1061
|
+
For complex pipelines, prefer chaining sin_bash calls over single
|
|
1062
|
+
shell pipelines — easier to debug, partial success possible.
|
|
1063
|
+
|
|
1064
|
+
Better than native bash: secret-safety, timeout, structured result.
|
|
1065
|
+
"""
|
|
1066
|
+
import shutil as _sh
|
|
1067
|
+
import subprocess as _sp
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
cmd_path = _sh.which("execute") or str(_Path.home() / ".local/bin/execute")
|
|
1071
|
+
if _Path(cmd_path).exists():
|
|
1072
|
+
proc = _sp.run(
|
|
1073
|
+
[cmd_path, "-timeout", str(timeout), "-format", "json", "-command", command],
|
|
1074
|
+
capture_output=True,
|
|
1075
|
+
text=True,
|
|
1076
|
+
timeout=timeout + 10,
|
|
1077
|
+
)
|
|
1078
|
+
return json.dumps(
|
|
1079
|
+
{
|
|
1080
|
+
"stdout": proc.stdout,
|
|
1081
|
+
"stderr": proc.stderr,
|
|
1082
|
+
"returncode": proc.returncode,
|
|
1083
|
+
"redacted": True,
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
proc = _sp.run(
|
|
1087
|
+
command,
|
|
1088
|
+
shell=True,
|
|
1089
|
+
capture_output=True,
|
|
1090
|
+
text=True,
|
|
1091
|
+
timeout=timeout,
|
|
1092
|
+
)
|
|
1093
|
+
return json.dumps(
|
|
1094
|
+
{
|
|
1095
|
+
"stdout": proc.stdout[-10000:],
|
|
1096
|
+
"stderr": proc.stderr[-5000:],
|
|
1097
|
+
"returncode": proc.returncode,
|
|
1098
|
+
"redacted": False,
|
|
1099
|
+
"warning": "execute binary not found — running raw shell",
|
|
1100
|
+
}
|
|
1101
|
+
)
|
|
1102
|
+
except _sp.TimeoutExpired:
|
|
1103
|
+
return json.dumps({"error": f"timeout after {timeout}s", "command": command})
|
|
1104
|
+
except Exception as exc:
|
|
1105
|
+
return json.dumps({"error": str(exc), "command": command})
|
|
1106
|
+
|
|
1107
|
+
@mcp.tool()
|
|
1108
|
+
def sin_search(query: str, path: str = ".", search_type: str = "semantic") -> str:
|
|
1109
|
+
"""SIN-Code search — replaces native search/grep.
|
|
1110
|
+
|
|
1111
|
+
Wraps the `scout` Go tool (semantic + regex + symbol search). Falls
|
|
1112
|
+
back to Python regex if scout binary is missing.
|
|
1113
|
+
|
|
1114
|
+
search_type: semantic | regex | symbol | usage
|
|
1115
|
+
|
|
1116
|
+
Accepts both directory paths (rglob) and single files (single file scan).
|
|
1117
|
+
"""
|
|
1118
|
+
import shutil as _sh
|
|
1119
|
+
import subprocess as _sp
|
|
1120
|
+
|
|
1121
|
+
try:
|
|
1122
|
+
cmd_path = _sh.which("scout") or str(_Path.home() / ".local/bin/scout")
|
|
1123
|
+
if _Path(cmd_path).exists():
|
|
1124
|
+
proc = _sp.run(
|
|
1125
|
+
[cmd_path, "--query", query, "--path", path, "--type", search_type, "--json"],
|
|
1126
|
+
capture_output=True,
|
|
1127
|
+
text=True,
|
|
1128
|
+
timeout=30,
|
|
1129
|
+
)
|
|
1130
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
1131
|
+
try:
|
|
1132
|
+
return proc.stdout
|
|
1133
|
+
except Exception:
|
|
1134
|
+
pass
|
|
1135
|
+
# fall through to python-regex fallback
|
|
1136
|
+
import re as _re
|
|
1137
|
+
|
|
1138
|
+
results = []
|
|
1139
|
+
target = _Path(path).expanduser()
|
|
1140
|
+
# Determine which files to scan
|
|
1141
|
+
if target.is_file():
|
|
1142
|
+
files = [target]
|
|
1143
|
+
elif target.is_dir():
|
|
1144
|
+
files = [p for p in target.rglob("*") if p.is_file() and ".git" not in p.parts]
|
|
1145
|
+
else:
|
|
1146
|
+
return json.dumps({"error": f"path not found: {path}"})
|
|
1147
|
+
for p in files:
|
|
1148
|
+
try:
|
|
1149
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
1150
|
+
except Exception:
|
|
1151
|
+
continue
|
|
1152
|
+
for m in _re.finditer(query, text):
|
|
1153
|
+
line_no = text[: m.start()].count("\n") + 1
|
|
1154
|
+
line_text = (
|
|
1155
|
+
text.splitlines()[line_no - 1] if line_no <= len(text.splitlines()) else ""
|
|
1156
|
+
)
|
|
1157
|
+
results.append(
|
|
1158
|
+
{
|
|
1159
|
+
"file": str(p),
|
|
1160
|
+
"line": line_no,
|
|
1161
|
+
"match": m.group(0),
|
|
1162
|
+
"context": line_text[:200],
|
|
1163
|
+
}
|
|
1164
|
+
)
|
|
1165
|
+
# 200 = hard ceiling for python-regex fallback; keeps
|
|
1166
|
+
# the fallback from flooding agent context on common
|
|
1167
|
+
# broad queries like `import `.
|
|
1168
|
+
if len(results) >= 200:
|
|
1169
|
+
break
|
|
1170
|
+
if len(results) >= 200:
|
|
1171
|
+
break
|
|
1172
|
+
return json.dumps(
|
|
1173
|
+
{"results": results, "count": len(results), "fallback": "python-regex"}
|
|
1174
|
+
)
|
|
1175
|
+
except Exception as exc:
|
|
1176
|
+
return json.dumps({"error": str(exc), "query": query})
|
|
1177
|
+
|
|
1178
|
+
typer.echo("[SIN-BUNDLE] MCP server starting (stdio).", err=True)
|
|
1179
|
+
mcp.run()
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
if __name__ == "__main__":
|
|
1183
|
+
app()
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
# ── SIN Bench (SWE-bench A/B harness) ──────────────────────────────────────
|
|
1187
|
+
@app.command()
|
|
1188
|
+
def bench(
|
|
1189
|
+
tasks: str | None = typer.Option(
|
|
1190
|
+
None, "--tasks", help="Path to a JSONL task file. Omit to use SWE-bench Lite."
|
|
1191
|
+
),
|
|
1192
|
+
limit: int = typer.Option(20, help="Max number of tasks to run per arm."),
|
|
1193
|
+
runner: str = typer.Option(
|
|
1194
|
+
"dry", help="Agent runner: 'dry' | 'opencode' | 'codex' | 'hermes'."
|
|
1195
|
+
),
|
|
1196
|
+
arms: str = typer.Option("control,sin", help="Comma-separated arms to run."),
|
|
1197
|
+
out: str | None = typer.Option(None, "--out", help="Write the full JSON report to this path."),
|
|
1198
|
+
):
|
|
1199
|
+
"""Run the SIN-Code A/B benchmark and report the resolved-rate delta."""
|
|
1200
|
+
from sin_code_bundle.bench import (
|
|
1201
|
+
DryRunRunner,
|
|
1202
|
+
format_report,
|
|
1203
|
+
load_swebench_lite,
|
|
1204
|
+
load_tasks_jsonl,
|
|
1205
|
+
run_benchmark,
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
if tasks:
|
|
1209
|
+
task_list = load_tasks_jsonl(Path(tasks), limit=limit)
|
|
1210
|
+
else:
|
|
1211
|
+
try:
|
|
1212
|
+
task_list = load_swebench_lite(limit=limit)
|
|
1213
|
+
except RuntimeError as exc:
|
|
1214
|
+
typer.echo(f"[SIN-BUNDLE] {exc}", err=True)
|
|
1215
|
+
raise typer.Exit(code=2)
|
|
1216
|
+
|
|
1217
|
+
if not task_list:
|
|
1218
|
+
typer.echo("[SIN-BUNDLE] No tasks loaded.", err=True)
|
|
1219
|
+
raise typer.Exit(code=2)
|
|
1220
|
+
|
|
1221
|
+
if runner == "dry":
|
|
1222
|
+
agent_runner = DryRunRunner()
|
|
1223
|
+
elif runner in ("opencode", "codex", "hermes"):
|
|
1224
|
+
agent_runner = _build_cli_runner(runner)
|
|
1225
|
+
else:
|
|
1226
|
+
typer.echo(f"[SIN-BUNDLE] Unknown runner '{runner}'.", err=True)
|
|
1227
|
+
raise typer.Exit(code=2)
|
|
1228
|
+
|
|
1229
|
+
arm_tuple = tuple(a.strip() for a in arms.split(",") if a.strip())
|
|
1230
|
+
|
|
1231
|
+
typer.echo(
|
|
1232
|
+
f"[SIN-BUNDLE] Running {len(task_list)} task(s) x {len(arm_tuple)} arm(s) "
|
|
1233
|
+
f"with '{runner}' runner..."
|
|
1234
|
+
)
|
|
1235
|
+
report = run_benchmark(task_list, agent_runner, arms=arm_tuple) # type: ignore[arg-type]
|
|
1236
|
+
typer.echo(format_report(report))
|
|
1237
|
+
|
|
1238
|
+
if out:
|
|
1239
|
+
Path(out).write_text(report.to_json(), encoding="utf-8")
|
|
1240
|
+
typer.echo(f"[SIN-BUNDLE] Wrote full report -> {out}")
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _build_cli_runner(agent: str):
|
|
1244
|
+
from sin_code_bundle.bench import CommandRunner
|
|
1245
|
+
|
|
1246
|
+
def build_cmd(task, sin_enabled: bool) -> list[str]:
|
|
1247
|
+
prompt = task.problem_statement
|
|
1248
|
+
if agent == "opencode":
|
|
1249
|
+
return ["opencode", "run", "-m", prompt]
|
|
1250
|
+
if agent == "codex":
|
|
1251
|
+
return ["codex", "exec", "--skip-git-repo-check", prompt]
|
|
1252
|
+
if agent == "hermes":
|
|
1253
|
+
return ["hermes", "run", "--prompt", prompt]
|
|
1254
|
+
raise ValueError(agent)
|
|
1255
|
+
|
|
1256
|
+
return CommandRunner(build_cmd=build_cmd, timeout_s=1800)
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
# ── SIN Hooks (automatic SIN-Brain calls via .opencode hooks) ──────────────
|
|
1260
|
+
@app.command(name="hooks-install")
|
|
1261
|
+
def hooks_install(
|
|
1262
|
+
target: str = typer.Argument("opencode", help="Target CLI: opencode"),
|
|
1263
|
+
pre_command: bool = typer.Option(True, "--pre-command", help="Install pre-command hook."),
|
|
1264
|
+
post_command: bool = typer.Option(True, "--post-command", help="Install post-command hook."),
|
|
1265
|
+
brain_path: str = typer.Option(
|
|
1266
|
+
".sin/brain.db", "--brain-path", help="SIN-Brain database path."
|
|
1267
|
+
),
|
|
1268
|
+
):
|
|
1269
|
+
"""Install automatic hooks for SIN-Brain calls before/after every command."""
|
|
1270
|
+
from sin_code_bundle import hooks
|
|
1271
|
+
|
|
1272
|
+
if target != "opencode":
|
|
1273
|
+
typer.echo("[SIN-BUNDLE] Only 'opencode' hooks are supported.", err=True)
|
|
1274
|
+
raise typer.Exit(code=2)
|
|
1275
|
+
|
|
1276
|
+
installed = hooks.install_opencode_hooks(
|
|
1277
|
+
pre_command=pre_command,
|
|
1278
|
+
post_command=post_command,
|
|
1279
|
+
brain_path=brain_path,
|
|
1280
|
+
)
|
|
1281
|
+
for path in installed:
|
|
1282
|
+
typer.echo(f"[SIN-BUNDLE] Installed hook -> {path}")
|
|
1283
|
+
if not installed:
|
|
1284
|
+
typer.echo(
|
|
1285
|
+
"[SIN-BUNDLE] No hooks installed (both --pre-command and --post-command disabled)."
|
|
1286
|
+
)
|
|
1287
|
+
else:
|
|
1288
|
+
typer.echo("[SIN-BUNDLE] Hooks active. Run `sin hooks-uninstall` to remove them.")
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
@app.command(name="hooks-uninstall")
|
|
1292
|
+
def hooks_uninstall(
|
|
1293
|
+
target: str = typer.Argument("opencode", help="Target CLI: opencode"),
|
|
1294
|
+
):
|
|
1295
|
+
"""Remove automatic SIN-Brain hooks from ~/.opencode/hooks/."""
|
|
1296
|
+
from sin_code_bundle import hooks
|
|
1297
|
+
|
|
1298
|
+
if target != "opencode":
|
|
1299
|
+
typer.echo("[SIN-BUNDLE] Only 'opencode' hooks are supported.", err=True)
|
|
1300
|
+
raise typer.Exit(code=2)
|
|
1301
|
+
|
|
1302
|
+
removed = hooks.uninstall_opencode_hooks()
|
|
1303
|
+
for path in removed:
|
|
1304
|
+
typer.echo(f"[SIN-BUNDLE] Removed hook -> {path}")
|
|
1305
|
+
if not removed:
|
|
1306
|
+
typer.echo("[SIN-BUNDLE] No hooks found to uninstall.")
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
@app.command(name="hooks-list")
|
|
1310
|
+
def hooks_list(
|
|
1311
|
+
target: str = typer.Argument("opencode", help="Target CLI: opencode"),
|
|
1312
|
+
):
|
|
1313
|
+
"""List installed SIN-Brain hooks in ~/.opencode/hooks/."""
|
|
1314
|
+
from sin_code_bundle import hooks
|
|
1315
|
+
|
|
1316
|
+
if target != "opencode":
|
|
1317
|
+
typer.echo("[SIN-BUNDLE] Only 'opencode' hooks are supported.", err=True)
|
|
1318
|
+
raise typer.Exit(code=2)
|
|
1319
|
+
|
|
1320
|
+
found = hooks.list_opencode_hooks()
|
|
1321
|
+
if not found:
|
|
1322
|
+
typer.echo("[SIN-BUNDLE] No hooks installed. Run `sin hooks-install` to set them up.")
|
|
1323
|
+
else:
|
|
1324
|
+
for path in found:
|
|
1325
|
+
typer.echo(f"[SIN-BUNDLE] Hook -> {path}")
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
# ── Skills (compile portable skills into an agent's native format) ─────────
|
|
1329
|
+
@app.command()
|
|
1330
|
+
def skills(
|
|
1331
|
+
target: str = typer.Argument(..., help="opencode | codex | claude | all"),
|
|
1332
|
+
source: str = typer.Option("skills", help="Source skills directory."),
|
|
1333
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview only."),
|
|
1334
|
+
):
|
|
1335
|
+
"""Compile portable SIN skills into an agent's native command/skill format."""
|
|
1336
|
+
from sin_code_bundle.skills import SUPPORTED_TARGETS, compile_skills
|
|
1337
|
+
|
|
1338
|
+
valid = SUPPORTED_TARGETS
|
|
1339
|
+
targets = list(valid) if target == "all" else [target] # type: ignore[list-item]
|
|
1340
|
+
for t in targets:
|
|
1341
|
+
if t not in valid:
|
|
1342
|
+
typer.echo(f"[SIN-BUNDLE] Unknown target '{t}'.", err=True)
|
|
1343
|
+
raise typer.Exit(code=2)
|
|
1344
|
+
paths = compile_skills(t, Path(source), dry_run=dry_run) # type: ignore[arg-type]
|
|
1345
|
+
verb = "Would write" if dry_run else "Wrote"
|
|
1346
|
+
for p in paths:
|
|
1347
|
+
typer.echo(f"[SIN-BUNDLE] {verb} {t} skill -> {p}")
|
|
1348
|
+
if not paths:
|
|
1349
|
+
typer.echo(f"[SIN-BUNDLE] No skills found in '{source}'.")
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
# ── Policy (inspect / initialize the policy and audit log) ─────────────────
|
|
1353
|
+
@app.command()
|
|
1354
|
+
def policy(
|
|
1355
|
+
action: str = typer.Argument("show", help="show | init | verify"),
|
|
1356
|
+
root: str = typer.Option(".", help="Project root."),
|
|
1357
|
+
):
|
|
1358
|
+
"""Inspect or initialize the SIN policy and audit log."""
|
|
1359
|
+
from sin_code_bundle.policy import DEFAULT_POLICY, AuditLog, Policy
|
|
1360
|
+
|
|
1361
|
+
root_path = Path(root)
|
|
1362
|
+
if action == "init":
|
|
1363
|
+
path = root_path / ".sin" / "policy.yaml"
|
|
1364
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1365
|
+
if path.exists():
|
|
1366
|
+
typer.echo(f"[SIN-BUNDLE] {path} already exists.")
|
|
1367
|
+
return
|
|
1368
|
+
try:
|
|
1369
|
+
import yaml as _yaml
|
|
1370
|
+
|
|
1371
|
+
path.write_text(
|
|
1372
|
+
_yaml.safe_dump(
|
|
1373
|
+
{"auto_approve": False, "rules": dict(DEFAULT_POLICY)},
|
|
1374
|
+
sort_keys=False,
|
|
1375
|
+
),
|
|
1376
|
+
encoding="utf-8",
|
|
1377
|
+
)
|
|
1378
|
+
except ImportError:
|
|
1379
|
+
# Manual fallback if pyyaml missing
|
|
1380
|
+
path.write_text(
|
|
1381
|
+
"auto_approve: false\nrules:\n"
|
|
1382
|
+
+ "".join(f" {k}: {v}\n" for k, v in DEFAULT_POLICY.items()),
|
|
1383
|
+
encoding="utf-8",
|
|
1384
|
+
)
|
|
1385
|
+
typer.echo(f"[SIN-BUNDLE] Wrote default policy -> {path}")
|
|
1386
|
+
return
|
|
1387
|
+
|
|
1388
|
+
if action == "verify":
|
|
1389
|
+
ok = AuditLog(root_path).verify_chain()
|
|
1390
|
+
typer.echo(f"[SIN-BUNDLE] Audit chain {'intact' if ok else 'TAMPERED'}.")
|
|
1391
|
+
raise typer.Exit(code=0 if ok else 1)
|
|
1392
|
+
|
|
1393
|
+
p = Policy.load(root_path)
|
|
1394
|
+
typer.echo("[SIN-BUNDLE] Effective policy:")
|
|
1395
|
+
for risk, decision in p.rules.items():
|
|
1396
|
+
typer.echo(f" {risk:<8} -> {decision}")
|
|
1397
|
+
typer.echo(f" auto_approve = {p.auto_approve}")
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
# ── Doctor (environment diagnostics) ──────────────────────────────────────
|
|
1401
|
+
@app.command()
|
|
1402
|
+
def doctor(root: str = typer.Option(".", help="Project root.")):
|
|
1403
|
+
"""Diagnose the environment: detected languages, LSP servers, audit chain."""
|
|
1404
|
+
from sin_code_bundle.lsp_bootstrap import server_status
|
|
1405
|
+
from sin_code_bundle.policy import AuditLog
|
|
1406
|
+
|
|
1407
|
+
rows = server_status(Path(root))
|
|
1408
|
+
typer.echo("[SIN-BUNDLE] Language servers (for accurate impact analysis):")
|
|
1409
|
+
if not rows:
|
|
1410
|
+
typer.echo(" (no supported source files detected)")
|
|
1411
|
+
for r in rows:
|
|
1412
|
+
mark = "OK " if r["installed"] else "-- "
|
|
1413
|
+
typer.echo(f" {mark}{r['language']:<11} {r['files']:>5} files server={r['server']}")
|
|
1414
|
+
if not r["installed"]:
|
|
1415
|
+
typer.echo(f" install: {r['install_hint']}")
|
|
1416
|
+
|
|
1417
|
+
ok = AuditLog(Path(root)).verify_chain()
|
|
1418
|
+
typer.echo(f"[SIN-BUNDLE] Audit chain: {'intact' if ok else 'TAMPERED'}")
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
# ── SIN-Code Go Tools Commands ─────────────────────────────────────────────
|
|
1422
|
+
@sin_code_app.command("run")
|
|
1423
|
+
def sin_code_run(
|
|
1424
|
+
tool: str = typer.Argument(
|
|
1425
|
+
..., help="Tool name: discover, execute, map, grasp, scout, harvest, orchestrate"
|
|
1426
|
+
),
|
|
1427
|
+
args: list[str] = typer.Argument(default_factory=list, help="Arguments to pass to the tool"),
|
|
1428
|
+
):
|
|
1429
|
+
"""Run a SIN-Code Go tool with the given arguments."""
|
|
1430
|
+
if tool not in _SIN_CODE_TOOLS:
|
|
1431
|
+
typer.echo(
|
|
1432
|
+
f"[SIN-CODE] Unknown tool: {tool}. Available: {', '.join(_SIN_CODE_TOOLS.keys())}",
|
|
1433
|
+
err=True,
|
|
1434
|
+
)
|
|
1435
|
+
raise typer.Exit(code=1)
|
|
1436
|
+
|
|
1437
|
+
path = _sin_code_tool_path(tool)
|
|
1438
|
+
if not path:
|
|
1439
|
+
typer.echo(
|
|
1440
|
+
f"[SIN-CODE] Tool '{tool}' not found. Install: go install github.com/OpenSIN-Code/{_SIN_CODE_TOOLS[tool]}/cmd/{tool}@latest",
|
|
1441
|
+
err=True,
|
|
1442
|
+
)
|
|
1443
|
+
raise typer.Exit(code=1)
|
|
1444
|
+
|
|
1445
|
+
result = subprocess.run([str(path)] + args, capture_output=True, text=True)
|
|
1446
|
+
if result.stdout:
|
|
1447
|
+
typer.echo(result.stdout)
|
|
1448
|
+
if result.stderr:
|
|
1449
|
+
typer.echo(result.stderr, err=True)
|
|
1450
|
+
raise typer.Exit(code=result.returncode)
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
@sin_code_app.command("agents-md")
|
|
1454
|
+
def sin_code_agents_md(
|
|
1455
|
+
output: Path = typer.Option(Path("AGENTS.md"), help="Output file path"),
|
|
1456
|
+
):
|
|
1457
|
+
"""Generate AGENTS.md with SIN-Code Tool Suite rules for all agents."""
|
|
1458
|
+
content = """# 🛠️ SIN-Code Tool Suite — IMMER UNSERE EIGENEN TOOLS NUTZEN (PRIORITY -10.0 - ABSOLUTE OBERSTE REGEL)
|
|
1459
|
+
|
|
1460
|
+
## ⚠️ ABSOLUTE REGEL: Niemals opencode-eigene Tools nutzen, wenn unsere SIN-Code Tools verfügbar sind!
|
|
1461
|
+
|
|
1462
|
+
**UNSERE 7 SIN-Code Tools sind BESSER als die opencode-eigenen Alternativen.** Jeder Agent MUSS unsere Tools nutzen, niemals die opencode-internen.
|
|
1463
|
+
|
|
1464
|
+
### Wann welches Tool?
|
|
1465
|
+
|
|
1466
|
+
| Aufgabe | SIN-Code Tool (NUTZEN!) | Opencode Alternative (NICHT nutzen!) | Warum unsere besser |
|
|
1467
|
+
|---------|------------------------|-----------------------------------|-------------------|
|
|
1468
|
+
| **Dateien suchen, Projekt-Struktur erkunden** | `sin-discover` | `opencode` interne Dateisuche | Pattern-Matching, Relevanz-Scoring, Dependency-Map, Gruppierung |
|
|
1469
|
+
| **Befehle ausführen, Kommandos laufen lassen** | `sin-execute` | `opencode` interne Shell-Ausführung | Secret-Redaction, Safety-Checks, Timeout-Handling, Error-Analyse |
|
|
1470
|
+
| **Architektur analysieren, Module mappen** | `sin-map` | `opencode` interne Code-Analyse | Module-Level, Entry-Points, Hot-Paths, Dependency-Graph, Orphan-Detection |
|
|
1471
|
+
| **Einzelne Datei verstehen, Code analysieren** | `sin-grasp` | `opencode` interne Code-Analyse | Struktur, Dependencies, Usage, Context, Related-Files |
|
|
1472
|
+
| **Code durchsuchen, Patterns finden** | `sin-scout` | `opencode` interne Suche | Regex, Semantic, Symbol, Usage-Search, Dead-Code-Detection |
|
|
1473
|
+
| **URLs abrufen, APIs konsumieren** | `sin-harvest` | `opencode` interne HTTP-Requests | Caching, Struktur-Extraktion, Change-Detection, Auth-Management |
|
|
1474
|
+
| **Tasks managen, Planung, Rollback** | `sin-orchestrate` | `opencode` interne Task-Planung | Dependencies, Parallel-Execution, Blocker-Detection, Rollback-Plan |
|
|
1475
|
+
|
|
1476
|
+
### Tool-Verweisung & Skills/MCP
|
|
1477
|
+
|
|
1478
|
+
| Tool | MCP Name | GitHub Repo | Skill | Installiert |
|
|
1479
|
+
|------|----------|-------------|-------|-------------|
|
|
1480
|
+
| `sin-discover` | `sin-discover` | `OpenSIN-Code/SIN-Code-Discover-Tool` | `sin-discover` | ✅ `~/.local/bin/discover` |
|
|
1481
|
+
| `sin-execute` | `sin-execute` | `OpenSIN-Code/SIN-Code-Execute-Tool` | `sin-execute` | ✅ `~/.local/bin/execute` |
|
|
1482
|
+
| `sin-map` | `sin-map` | `OpenSIN-Code/SIN-Code-Map-Tool` | `sin-map` | ✅ `~/.local/bin/map` |
|
|
1483
|
+
| `sin-grasp` | `sin-grasp` | `OpenSIN-Code/SIN-Code-Grasp-Tool` | `sin-grasp` | ✅ `~/.local/bin/grasp` |
|
|
1484
|
+
| `sin-scout` | `sin-scout` | `OpenSIN-Code/SIN-Code-Scout-Tool` | `sin-scout` | ✅ `~/.local/bin/scout` |
|
|
1485
|
+
| `sin-harvest` | `sin-harvest` | `OpenSIN-Code/SIN-Code-Harvest-Tool` | `sin-harvest` | ✅ `~/.local/bin/harvest` |
|
|
1486
|
+
| `sin-orchestrate` | `sin-orchestrate` | `OpenSIN-Code/SIN-Code-Orchestrate-Tool` | `sin-orchestrate` | ✅ `~/.local/bin/orchestrate` |
|
|
1487
|
+
|
|
1488
|
+
### Anwendungsbeispiele
|
|
1489
|
+
|
|
1490
|
+
**1. Neues Projekt erkunden:**
|
|
1491
|
+
```bash
|
|
1492
|
+
# NIEMALS opencode-interne Dateisuche nutzen!
|
|
1493
|
+
/Users/jeremy/.local/bin/discover -path /Users/jeremy/dev/NEUES-PROJEKT -pattern "**/*.py" -sort_by relevance -format json
|
|
1494
|
+
# Ergebnis: Alle Python-Dateien absteigend nach Relevanz sortiert, mit Dependencies und Related-Files
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
**2. Befehle sicher ausführen:**
|
|
1498
|
+
```bash
|
|
1499
|
+
# NIEMALS opencode-interne Shell-Ausführung nutzen!
|
|
1500
|
+
/Users/jeremy/.local/bin/execute -command "npm test" -timeout 60 -format json
|
|
1501
|
+
# Ergebnis: Safety-Check, Secret-Redaction, Error-Analyse, Timeout-Handling
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
**3. Architektur verstehen:**
|
|
1505
|
+
```bash
|
|
1506
|
+
# NIEMALS opencode-interne Code-Analyse nutzen!
|
|
1507
|
+
/Users/jeremy/.local/bin/map -path /Users/jeremy/dev/PROJEKT -action map -format json
|
|
1508
|
+
# Ergebnis: Module, Entry-Points, Hot-Paths, Dependency-Graph, Orphan-Detection, Complexity
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
**4. Code durchsuchen:**
|
|
1512
|
+
```bash
|
|
1513
|
+
# NIEMALS opencode-interne Suche nutzen!
|
|
1514
|
+
/Users/jeremy/.local/bin/scout -query "func.*main" -path /Users/jeremy/dev/PROJEKT -search_type regex -format json
|
|
1515
|
+
# Ergebnis: Alle Main-Funktionen, mit Usage-Count, Architecture-Info, Context
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
**5. API/URL abrufen:**
|
|
1519
|
+
```bash
|
|
1520
|
+
# NIEMALS opencode-interne HTTP-Requests nutzen!
|
|
1521
|
+
/Users/jeremy/.local/bin/harvest -url "https://api.example.com/data" -format json
|
|
1522
|
+
# Ergebnis: Status, Body, Headers, Struktur-Extraktion, Caching
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
**6. Tasks planen & tracken:**
|
|
1526
|
+
```bash
|
|
1527
|
+
# NIEMALS opencode-interne Task-Planung nutzen!
|
|
1528
|
+
/Users/jeremy/.local/bin/orchestrate -action add -title "Feature implementieren" -tags "urgent" -format json
|
|
1529
|
+
# Ergebnis: Task-ID, Dependencies, Parallel-Execution-Plan, Rollback-Plan
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
### Regeln
|
|
1533
|
+
|
|
1534
|
+
- **SOFORTIGER PERMANENTER BAN** bei Nutzung von opencode-internen Tools statt unserer SIN-Code Tools
|
|
1535
|
+
- **VOR jedem Coding-Task:** Erst mit `sin-discover` das Projekt erkunden, dann mit `sin-grasp` die relevante Datei verstehen
|
|
1536
|
+
- **VOR jedem Refactoring:** Erst mit `sin-map` die Architektur analysieren, dann mit `sin-scout` alle Referenzen finden
|
|
1537
|
+
- **VOR jedem Befehl:** Immer `sin-execute` mit Safety-Check und Secret-Redaction nutzen
|
|
1538
|
+
- **VOR jedem API-Call:** Immer `sin-harvest` mit Caching und Struktur-Extraktion nutzen
|
|
1539
|
+
- **Für jede Planung:** Immer `sin-orchestrate` mit Dependencies und Rollback-Plan nutzen
|
|
1540
|
+
|
|
1541
|
+
### Versionen (aktuell)
|
|
1542
|
+
|
|
1543
|
+
| Tool | Version | Status |
|
|
1544
|
+
|------|---------|--------|
|
|
1545
|
+
| sin-discover | v0.2.5-fixes | ✅ Stable |
|
|
1546
|
+
| sin-execute | v0.2.4-fixes | ✅ Stable |
|
|
1547
|
+
| sin-map | v0.2.5-fixes | ✅ Stable |
|
|
1548
|
+
| sin-grasp | v0.2.4-fixes | ✅ Stable |
|
|
1549
|
+
| sin-scout | v0.1.5-fixes | ✅ Stable |
|
|
1550
|
+
| sin-harvest | v0.1.4-fixes | ✅ Stable |
|
|
1551
|
+
| sin-orchestrate | v0.1.6-fixes | ✅ Stable |
|
|
1552
|
+
|
|
1553
|
+
---
|
|
1554
|
+
|
|
1555
|
+
"""
|
|
1556
|
+
output.write_text(content, encoding="utf-8")
|
|
1557
|
+
typer.echo(f"[SIN-CODE] Generated {output}")
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
# ── CEO Audit Sub-Commands (SOTA repo review) ─────────────────────────────
|
|
1561
|
+
_CEO_AUDIT_SKILL_PATH = Path.home() / ".config" / "opencode" / "skills" / "ceo-audit"
|
|
1562
|
+
_CEO_AUDIT_SCRIPT = _CEO_AUDIT_SKILL_PATH / "scripts" / "audit.sh"
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
@ceo_audit_app.command("run")
|
|
1566
|
+
def ceo_audit_run(
|
|
1567
|
+
repo: str = typer.Argument(".", help="Path to the repository to audit"),
|
|
1568
|
+
profile: str = typer.Option("FULL", "--profile", help="FULL | SECURITY | RELEASE | QUICK"),
|
|
1569
|
+
grade: str = typer.Option("", "--grade", help="CI grade gate: A | B | C"),
|
|
1570
|
+
output: str = typer.Option("", "--output", help="Output directory (default: ~/ceo-audits/)"),
|
|
1571
|
+
json_out: bool = typer.Option(False, "--json", help="Also write JSON sidecar"),
|
|
1572
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable ANSI colors"),
|
|
1573
|
+
):
|
|
1574
|
+
"""Run a 47-gate, 8-axis SOTA audit on a repository.
|
|
1575
|
+
|
|
1576
|
+
Requires the ceo-audit skill to be installed (run `sin ceo-audit install`).
|
|
1577
|
+
"""
|
|
1578
|
+
if not _CEO_AUDIT_SCRIPT.exists():
|
|
1579
|
+
typer.echo(
|
|
1580
|
+
f"[CEO-AUDIT] Skill not installed at {_CEO_AUDIT_SKILL_PATH}.\n"
|
|
1581
|
+
f" Install: sin ceo-audit install",
|
|
1582
|
+
err=True,
|
|
1583
|
+
)
|
|
1584
|
+
raise typer.Exit(code=4)
|
|
1585
|
+
|
|
1586
|
+
args = [str(_CEO_AUDIT_SCRIPT), f"--profile={profile}"]
|
|
1587
|
+
if grade:
|
|
1588
|
+
args.append(f"--grade={grade}")
|
|
1589
|
+
if output:
|
|
1590
|
+
args.append(f"--output={output}")
|
|
1591
|
+
if json_out:
|
|
1592
|
+
args.append("--json")
|
|
1593
|
+
if no_color:
|
|
1594
|
+
args.append("--no-color")
|
|
1595
|
+
args.append(repo)
|
|
1596
|
+
|
|
1597
|
+
result = subprocess.run(args)
|
|
1598
|
+
raise typer.Exit(code=result.returncode)
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
@ceo_audit_app.command("install")
|
|
1602
|
+
def ceo_audit_install(
|
|
1603
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing files"),
|
|
1604
|
+
):
|
|
1605
|
+
"""Install the ceo-audit skill to ~/.config/opencode/skills/ceo-audit/.
|
|
1606
|
+
|
|
1607
|
+
Idempotent: safe to run multiple times. Use --force to overwrite.
|
|
1608
|
+
"""
|
|
1609
|
+
import shutil
|
|
1610
|
+
|
|
1611
|
+
skill_source = Path(__file__).parent.parent.parent.parent / "skills" / "ceo-audit"
|
|
1612
|
+
skill_target = _CEO_AUDIT_SKILL_PATH
|
|
1613
|
+
|
|
1614
|
+
if not skill_source.exists():
|
|
1615
|
+
# Fall back: try the repo's skills/ directory
|
|
1616
|
+
skill_source = Path("/Users/jeremy/dev/SIN-Code-Bundle/skills/ceo-audit")
|
|
1617
|
+
if not skill_source.exists():
|
|
1618
|
+
typer.echo(
|
|
1619
|
+
f"[CEO-AUDIT] Cannot find ceo-audit skill source. Looked in:\n {skill_source}",
|
|
1620
|
+
err=True,
|
|
1621
|
+
)
|
|
1622
|
+
raise typer.Exit(code=1)
|
|
1623
|
+
|
|
1624
|
+
skill_target.parent.mkdir(parents=True, exist_ok=True)
|
|
1625
|
+
if skill_target.exists() and not force:
|
|
1626
|
+
typer.echo(f"[CEO-AUDIT] Skill already installed at {skill_target}")
|
|
1627
|
+
typer.echo(" Use --force to overwrite.")
|
|
1628
|
+
raise typer.Exit(code=0)
|
|
1629
|
+
|
|
1630
|
+
shutil.copytree(skill_source, skill_target, dirs_exist_ok=True)
|
|
1631
|
+
# Make all scripts executable
|
|
1632
|
+
for script in (skill_target / "scripts").glob("*.sh"):
|
|
1633
|
+
script.chmod(0o755)
|
|
1634
|
+
if (skill_target / "hooks" / "post_audit.py").exists():
|
|
1635
|
+
(skill_target / "hooks" / "post_audit.py").chmod(0o755)
|
|
1636
|
+
typer.echo(f"[CEO-AUDIT] Installed to {skill_target}")
|
|
1637
|
+
typer.echo(" Run: sin ceo-audit run /path/to/repo")
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
@ceo_audit_app.command("status")
|
|
1641
|
+
def ceo_audit_status():
|
|
1642
|
+
"""Show whether the ceo-audit skill is installed and ready."""
|
|
1643
|
+
installed = _CEO_AUDIT_SCRIPT.exists()
|
|
1644
|
+
typer.echo(f"CEO Audit skill installed: {'yes' if installed else 'no'}")
|
|
1645
|
+
if installed:
|
|
1646
|
+
typer.echo(f" Path: {_CEO_AUDIT_SKILL_PATH}")
|
|
1647
|
+
# Check if SIN-Code tools are available
|
|
1648
|
+
from shutil import which
|
|
1649
|
+
|
|
1650
|
+
missing = [t for t in _SIN_CODE_TOOLS if not which(t)]
|
|
1651
|
+
if missing:
|
|
1652
|
+
typer.echo(f" Missing SIN-Code tools: {', '.join(missing)}")
|
|
1653
|
+
typer.echo(" Install: bash ~/.local/share/SIN-Code-Bundle/install.sh")
|
|
1654
|
+
else:
|
|
1655
|
+
typer.echo(" All 7 SIN-Code tools available")
|
|
1656
|
+
else:
|
|
1657
|
+
typer.echo(" Install: sin ceo-audit install")
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
# ── sin-browser Sub-Commands (106 browser-automation tools) ────────────────
|
|
1661
|
+
browser_app = typer.Typer(
|
|
1662
|
+
help="sin-browser — 106 browser-automation tools (navigate, click, fill, screenshot, scrape, etc.)"
|
|
1663
|
+
)
|
|
1664
|
+
app.add_typer(browser_app, name="browser")
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
@browser_app.command("list")
|
|
1668
|
+
def browser_list(
|
|
1669
|
+
filter: str = typer.Option(
|
|
1670
|
+
"", "--filter", help="Substring filter (e.g. 'click', 'screenshot')"
|
|
1671
|
+
),
|
|
1672
|
+
json_out: bool = typer.Option(False, "--json", help="Output full JSON instead of summary"),
|
|
1673
|
+
):
|
|
1674
|
+
"""List all 106 sin-browser-tools. Always run this first to discover the surface."""
|
|
1675
|
+
if not shutil.which("sin-browser"):
|
|
1676
|
+
typer.echo(
|
|
1677
|
+
"[BROWSER] sin-browser not installed. Install: https://github.com/OpenSIN-Code/SIN-Browser-Tools",
|
|
1678
|
+
err=True,
|
|
1679
|
+
)
|
|
1680
|
+
raise typer.Exit(code=1)
|
|
1681
|
+
result = subprocess.run(["sin-browser", "skills"], capture_output=True, text=True, timeout=30)
|
|
1682
|
+
if result.returncode != 0:
|
|
1683
|
+
typer.echo(f"[BROWSER] sin-browser failed: {result.stderr}", err=True)
|
|
1684
|
+
raise typer.Exit(code=1)
|
|
1685
|
+
import json as _json
|
|
1686
|
+
|
|
1687
|
+
data = _json.loads(result.stdout)
|
|
1688
|
+
actions = data.get("actions", {})
|
|
1689
|
+
if filter:
|
|
1690
|
+
actions = {
|
|
1691
|
+
k: v
|
|
1692
|
+
for k, v in actions.items()
|
|
1693
|
+
if filter.lower() in k.lower() or filter.lower() in v.get("description", "").lower()
|
|
1694
|
+
}
|
|
1695
|
+
if json_out:
|
|
1696
|
+
typer.echo(_json.dumps(actions, indent=2))
|
|
1697
|
+
else:
|
|
1698
|
+
from collections import defaultdict
|
|
1699
|
+
|
|
1700
|
+
by_cat: dict[str, list] = defaultdict(list)
|
|
1701
|
+
for name, tool in actions.items():
|
|
1702
|
+
by_cat[tool.get("category", "other")].append((name, tool.get("description", "")))
|
|
1703
|
+
typer.echo(f"\n sin-browser-tools -- {len(actions)} tools\n")
|
|
1704
|
+
for cat in sorted(by_cat):
|
|
1705
|
+
typer.echo(f"[{cat}] ({len(by_cat[cat])})")
|
|
1706
|
+
for name, desc in sorted(by_cat[cat]):
|
|
1707
|
+
desc_short = desc[:55] + "..." if len(desc) > 55 else desc
|
|
1708
|
+
typer.echo(f" - {name:35s} {desc_short}")
|
|
1709
|
+
typer.echo("")
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
@browser_app.command("help")
|
|
1713
|
+
def browser_help():
|
|
1714
|
+
"""Show sin-browser help."""
|
|
1715
|
+
if not shutil.which("sin-browser"):
|
|
1716
|
+
typer.echo("[BROWSER] sin-browser not installed", err=True)
|
|
1717
|
+
raise typer.Exit(code=1)
|
|
1718
|
+
subprocess.run(["sin-browser", "help"])
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
@browser_app.command("install-skill")
|
|
1722
|
+
def browser_install_skill():
|
|
1723
|
+
"""Install the sin-browser-tools skill to ~/.config/opencode/skills/."""
|
|
1724
|
+
import shutil
|
|
1725
|
+
|
|
1726
|
+
skill_source = Path(__file__).parent.parent.parent.parent / "skills" / "sin-browser-tools"
|
|
1727
|
+
skill_target = Path.home() / ".config" / "opencode" / "skills" / "sin-browser-tools"
|
|
1728
|
+
if not skill_source.exists():
|
|
1729
|
+
skill_source = Path("/Users/jeremy/dev/Infra-SIN-OpenCode-Stack/skills/sin-browser-tools")
|
|
1730
|
+
if not skill_source.exists():
|
|
1731
|
+
typer.echo("[BROWSER] Cannot find skill source", err=True)
|
|
1732
|
+
raise typer.Exit(code=1)
|
|
1733
|
+
skill_target.parent.mkdir(parents=True, exist_ok=True)
|
|
1734
|
+
shutil.copytree(skill_source, skill_target, dirs_exist_ok=True)
|
|
1735
|
+
for script in (skill_target / "scripts").glob("*.py"):
|
|
1736
|
+
script.chmod(0o755)
|
|
1737
|
+
typer.echo(f"[BROWSER] Installed skill to {skill_target}")
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
@browser_app.command("status")
|
|
1741
|
+
def browser_status():
|
|
1742
|
+
"""Show sin-browser status."""
|
|
1743
|
+
if not shutil.which("sin-browser"):
|
|
1744
|
+
typer.echo("sin-browser installed: no")
|
|
1745
|
+
typer.echo(" Install: https://github.com/OpenSIN-Code/SIN-Browser-Tools")
|
|
1746
|
+
raise typer.Exit(code=1)
|
|
1747
|
+
result = subprocess.run(["sin-browser", "skills"], capture_output=True, text=True, timeout=10)
|
|
1748
|
+
if result.returncode != 0:
|
|
1749
|
+
typer.echo("sin-browser installed: yes (but broken)")
|
|
1750
|
+
typer.echo(f" Error: {result.stderr[:200]}")
|
|
1751
|
+
raise typer.Exit(code=1)
|
|
1752
|
+
import json as _json
|
|
1753
|
+
|
|
1754
|
+
try:
|
|
1755
|
+
data = _json.loads(result.stdout)
|
|
1756
|
+
count = data.get("count", 0)
|
|
1757
|
+
except Exception:
|
|
1758
|
+
count = "?"
|
|
1759
|
+
typer.echo(f"sin-browser installed: yes ({count} tools available)")
|
|
1760
|
+
skill = Path.home() / ".config" / "opencode" / "skills" / "sin-browser-tools" / "SKILL.md"
|
|
1761
|
+
typer.echo(f" Skill installed: {'yes' if skill.exists() else 'no'}")
|
|
1762
|
+
typer.echo(" See: sin browser list")
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
# ── v2 Sub-Commands (VFS, Hashline, Memory, AST) ───────────────────────────
|
|
1766
|
+
vfs_app = typer.Typer(
|
|
1767
|
+
help="VFS — resolve SIN URI schemes (sckg://, poc://, ibd://, adw://, efsm://, oracle://, conflict://)"
|
|
1768
|
+
)
|
|
1769
|
+
app.add_typer(vfs_app, name="vfs")
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
@vfs_app.command("resolve")
|
|
1773
|
+
def vfs_resolve(
|
|
1774
|
+
uri: str = typer.Argument(..., help="URI to resolve (e.g., sckg://module/auth/dependencies)"),
|
|
1775
|
+
repo: str = typer.Option(".", "--repo", help="Repo root"),
|
|
1776
|
+
json_out: bool = typer.Option(False, "--json", help="JSON output"),
|
|
1777
|
+
):
|
|
1778
|
+
"""Resolve a SIN URI scheme to structured content."""
|
|
1779
|
+
from sin_code_bundle.vfs import SINVirtualFS
|
|
1780
|
+
|
|
1781
|
+
vfs = SINVirtualFS(Path(repo))
|
|
1782
|
+
result = vfs.resolve(uri)
|
|
1783
|
+
typer.echo(json.dumps(result, indent=2))
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
@vfs_app.command("schemes")
|
|
1787
|
+
def vfs_schemes():
|
|
1788
|
+
"""List all available URI schemes."""
|
|
1789
|
+
from sin_code_bundle.vfs import URI_SCHEMES
|
|
1790
|
+
|
|
1791
|
+
typer.echo("Available URI schemes:")
|
|
1792
|
+
for scheme, desc in URI_SCHEMES.items():
|
|
1793
|
+
typer.echo(f" {scheme}:// {desc}")
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
@vfs_app.command("status")
|
|
1797
|
+
def vfs_status():
|
|
1798
|
+
"""Check which SIN subsystems are available for VFS resolution."""
|
|
1799
|
+
from sin_code_bundle.vfs import URI_SCHEMES
|
|
1800
|
+
|
|
1801
|
+
typer.echo("VFS backend status:")
|
|
1802
|
+
module_map = {
|
|
1803
|
+
"sckg": "sin_code_sckg",
|
|
1804
|
+
"poc": "sin_code_poc",
|
|
1805
|
+
"ibd": "sin_code_ibd",
|
|
1806
|
+
"adw": "sin_code_adw",
|
|
1807
|
+
"efsm": "sin_code_efsm",
|
|
1808
|
+
"oracle": "sin_code_oracle",
|
|
1809
|
+
}
|
|
1810
|
+
for scheme in URI_SCHEMES:
|
|
1811
|
+
if scheme == "conflict":
|
|
1812
|
+
typer.echo(f" {scheme:8s} OK (git-based)")
|
|
1813
|
+
continue
|
|
1814
|
+
try:
|
|
1815
|
+
__import__(module_map[scheme])
|
|
1816
|
+
typer.echo(f" {scheme:8s} OK")
|
|
1817
|
+
except ImportError:
|
|
1818
|
+
typer.echo(f" {scheme:8s} NOT INSTALLED")
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
hashline_app = typer.Typer(
|
|
1822
|
+
help="Hashline anchor patching — content-hash based, no string-not-found errors"
|
|
1823
|
+
)
|
|
1824
|
+
app.add_typer(hashline_app, name="hashline")
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
@hashline_app.command("patch")
|
|
1828
|
+
def hashline_patch(
|
|
1829
|
+
file: Path = typer.Argument(..., help="File to patch"),
|
|
1830
|
+
old: str = typer.Option(..., "--old", help="Old content to replace"),
|
|
1831
|
+
new: str = typer.Option(..., "--new", help="New content"),
|
|
1832
|
+
intent: str = typer.Option("", "--intent", help="Intent description"),
|
|
1833
|
+
apply: bool = typer.Option(False, "--apply", help="Apply the patch immediately"),
|
|
1834
|
+
json_out: bool = typer.Option(False, "--json", help="JSON output"),
|
|
1835
|
+
):
|
|
1836
|
+
"""Create a hashline-anchored patch (and optionally apply it)."""
|
|
1837
|
+
from sin_code_bundle.hashline import SINHashlinePatch
|
|
1838
|
+
|
|
1839
|
+
patcher = SINHashlinePatch()
|
|
1840
|
+
patch = patcher.create_semantic_patch(file, old, new, intent or None)
|
|
1841
|
+
if patch is None:
|
|
1842
|
+
typer.echo(f"ERROR: Could not find anchor for old content in {file}", err=True)
|
|
1843
|
+
raise typer.Exit(code=1)
|
|
1844
|
+
if apply:
|
|
1845
|
+
success, msg = patcher.apply_semantic_patch(patch)
|
|
1846
|
+
result = {"patch": patch, "applied": success, "message": msg}
|
|
1847
|
+
else:
|
|
1848
|
+
result = {"patch": patch, "applied": False, "message": "Use --apply to write"}
|
|
1849
|
+
if json_out:
|
|
1850
|
+
typer.echo(json.dumps(result, indent=2))
|
|
1851
|
+
else:
|
|
1852
|
+
typer.echo(f"Patch: anchor_line={patch['anchor_line']}, hash={patch['anchor_hash'][:8]}")
|
|
1853
|
+
typer.echo(f"Status: {result['message']}")
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
@hashline_app.command("validate")
|
|
1857
|
+
def hashline_validate(
|
|
1858
|
+
file: Path = typer.Argument(..., help="File to validate against"),
|
|
1859
|
+
patch_json: str = typer.Option(..., "--patch", help="Patch JSON (or @file)"),
|
|
1860
|
+
):
|
|
1861
|
+
"""Validate a patch can still be applied (anchor not stale)."""
|
|
1862
|
+
from sin_code_bundle.hashline import HashlineAnchor
|
|
1863
|
+
|
|
1864
|
+
if patch_json.startswith("@"):
|
|
1865
|
+
with open(patch_json[1:]) as f:
|
|
1866
|
+
patch = json.load(f)
|
|
1867
|
+
else:
|
|
1868
|
+
patch = json.loads(patch_json)
|
|
1869
|
+
content = file.read_text()
|
|
1870
|
+
anchor = HashlineAnchor(content)
|
|
1871
|
+
is_valid, msg = anchor.validate_patch(patch)
|
|
1872
|
+
typer.echo(f"Valid: {is_valid} - {msg}")
|
|
1873
|
+
raise typer.Exit(code=0 if is_valid else 1)
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
# NOTE: The `sin memory {retain,recall,reflect,stats,forget}` and
|
|
1877
|
+
# `sin memory {honcho-status,honcho-retain,honcho-chat}` + `sin context query`
|
|
1878
|
+
# sub-commands were removed in this commit. They referenced `SINMemory` and
|
|
1879
|
+
# `HonchoBackend` classes that were moved to the external `sin-brain` package
|
|
1880
|
+
# (see commit af69464, BR-1, Issue #14). The bundle's `memory.py` is now a
|
|
1881
|
+
# thin pass-through adapter to `sin_brain.mcp_tools` and exposes the five
|
|
1882
|
+
# memory operations only as MCP tools (`recall`, `remember`, `forget`, `pin`,
|
|
1883
|
+
# `link_evidence`) registered by `sin serve` — not as CLI sub-commands.
|
|
1884
|
+
# Honcho integration is intentionally out of scope for this bundle: the
|
|
1885
|
+
# real memory backend is `sin-brain` (SQLite + FTS5, MIT, 1500+ LOC).
|
|
1886
|
+
# See `src/sin_code_bundle/memory.doc.md` for the current architecture.
|
|
1887
|
+
|
|
1888
|
+
ast_app = typer.Typer(help="AST-based code editing (requires tree-sitter)")
|
|
1889
|
+
app.add_typer(ast_app, name="ast")
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
@ast_app.command("edit")
|
|
1893
|
+
def ast_edit(
|
|
1894
|
+
file: Path = typer.Argument(..., help="File to edit"),
|
|
1895
|
+
old: str = typer.Option(..., "--old", help="Old substring"),
|
|
1896
|
+
new: str = typer.Option(..., "--new", help="Replacement"),
|
|
1897
|
+
apply: bool = typer.Option(False, "--apply", help="Apply changes immediately"),
|
|
1898
|
+
no_poc: bool = typer.Option(False, "--no-poc", help="Skip POC verification"),
|
|
1899
|
+
json_out: bool = typer.Option(False, "--json", help="JSON output"),
|
|
1900
|
+
):
|
|
1901
|
+
"""Propose an AST-based edit."""
|
|
1902
|
+
from sin_code_bundle.ast_edit import SINASTEdit
|
|
1903
|
+
|
|
1904
|
+
ast = SINASTEdit()
|
|
1905
|
+
if not ast.is_available():
|
|
1906
|
+
typer.echo(
|
|
1907
|
+
"ERROR: tree-sitter not installed. Run: pip install tree-sitter tree-sitter-languages",
|
|
1908
|
+
err=True,
|
|
1909
|
+
)
|
|
1910
|
+
raise typer.Exit(code=1)
|
|
1911
|
+
result = ast.edit(file, old, new, verify_with_poc=not no_poc)
|
|
1912
|
+
if apply and result.success:
|
|
1913
|
+
ast.resolve(file, result.proposed_changes)
|
|
1914
|
+
out = result.to_dict()
|
|
1915
|
+
if json_out:
|
|
1916
|
+
typer.echo(json.dumps(out, indent=2))
|
|
1917
|
+
else:
|
|
1918
|
+
if result.success:
|
|
1919
|
+
typer.echo(
|
|
1920
|
+
f"Edit proposed: {len(result.proposed_changes)} changes, POC verified={result.poc_verified}"
|
|
1921
|
+
)
|
|
1922
|
+
if apply:
|
|
1923
|
+
typer.echo("Applied.")
|
|
1924
|
+
else:
|
|
1925
|
+
typer.echo(f"ERROR: {result.error}", err=True)
|
|
1926
|
+
raise typer.Exit(code=1)
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
@ast_app.command("status")
|
|
1930
|
+
def ast_status():
|
|
1931
|
+
"""Check if AST edit is available."""
|
|
1932
|
+
from sin_code_bundle.ast_edit import SINASTEdit
|
|
1933
|
+
|
|
1934
|
+
ast = SINASTEdit()
|
|
1935
|
+
if ast.is_available():
|
|
1936
|
+
typer.echo(f"AST edit available. Languages: {', '.join(ast.SUPPORTED_LANGS)}")
|
|
1937
|
+
else:
|
|
1938
|
+
typer.echo("AST edit NOT available. Run: pip install tree-sitter tree-sitter-languages")
|
|
1939
|
+
raise typer.Exit(code=1)
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
if __name__ == "__main__":
|
|
1943
|
+
app()
|