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.
Files changed (41) hide show
  1. sin_code_bundle/__init__.py +6 -0
  2. sin_code_bundle/agents_md.py +245 -0
  3. sin_code_bundle/ast_edit.py +323 -0
  4. sin_code_bundle/bench.py +506 -0
  5. sin_code_bundle/budget.py +51 -0
  6. sin_code_bundle/cache.py +131 -0
  7. sin_code_bundle/checkpoint.py +230 -0
  8. sin_code_bundle/cli.py +1943 -0
  9. sin_code_bundle/codocs.py +328 -0
  10. sin_code_bundle/dap_bridge.py +135 -0
  11. sin_code_bundle/data/codocs/SKILL.md +280 -0
  12. sin_code_bundle/gitnexus.py +368 -0
  13. sin_code_bundle/hashline.py +216 -0
  14. sin_code_bundle/hooks.py +249 -0
  15. sin_code_bundle/immortal_commit.py +288 -0
  16. sin_code_bundle/interceptor.py +119 -0
  17. sin_code_bundle/lsp_backend.py +303 -0
  18. sin_code_bundle/lsp_bootstrap.py +85 -0
  19. sin_code_bundle/markitdown.py +254 -0
  20. sin_code_bundle/mcp_config.py +455 -0
  21. sin_code_bundle/mcp_server.py +963 -0
  22. sin_code_bundle/memory.py +208 -0
  23. sin_code_bundle/merge_safety.py +313 -0
  24. sin_code_bundle/orchestration_worktrees.py +102 -0
  25. sin_code_bundle/policy.py +224 -0
  26. sin_code_bundle/preflight.py +152 -0
  27. sin_code_bundle/programming_workflow.py +541 -0
  28. sin_code_bundle/rtk.py +154 -0
  29. sin_code_bundle/safety.py +52 -0
  30. sin_code_bundle/session_warmup.py +247 -0
  31. sin_code_bundle/skills.py +188 -0
  32. sin_code_bundle/symbol_resolve.py +166 -0
  33. sin_code_bundle/tools/__init__.py +4 -0
  34. sin_code_bundle/tools/pypi_setup.py +289 -0
  35. sin_code_bundle/vfs.py +264 -0
  36. sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
  37. sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
  38. sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
  39. sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
  40. sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
  41. 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()