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
@@ -0,0 +1,289 @@
1
+ # Purpose: One-click PyPI Trusted Publisher registration via API token.
2
+ # Docs: pypi_setup.doc.md
3
+ """One-click PyPI Trusted Publisher setup via API token.
4
+
5
+ Replaces the manual flow at https://pypi.org/manage/account/publishing/
6
+ with a single CLI invocation. After setup, every `git tag v*` + `git push`
7
+ auto-publishes to PyPI via GitHub Actions OIDC (no API token needed in CI).
8
+
9
+ Docs: pypi_setup.doc.md
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import re
17
+ import sys
18
+ import urllib.error
19
+ import urllib.request
20
+ from typing import Any, Dict, Optional, Tuple
21
+
22
+ PYPI_API = "https://pypi.org"
23
+ PUBLISHER_ENDPOINT = f"{PYPI_API}/_/v1/publisher"
24
+ FALLBACK_URL = "https://pypi.org/manage/account/publishing/"
25
+
26
+
27
+ def normalise_project_name(name: str) -> str:
28
+ """Normalise a project name per PEP 503.
29
+
30
+ PyPI normalises project names to lowercase, replacing runs of
31
+ ``.``, ``_``, ``-`` with a single ``-``. The Trusted Publisher
32
+ registration endpoint requires the normalised form.
33
+ """
34
+ return re.sub(r"[-_.]+", "-", name).lower()
35
+
36
+
37
+ def get_pending_publisher_payload(
38
+ project: str,
39
+ owner: str,
40
+ repo: str,
41
+ workflow: str,
42
+ environment: str,
43
+ ) -> Dict[str, Any]:
44
+ """Build the JSON payload for a new pending Trusted Publisher.
45
+
46
+ Args:
47
+ project: PyPI project name (normalised to PEP 503 form).
48
+ owner: GitHub owner (org or user). May be ``org`` or ``user/repo``.
49
+ repo: GitHub repository name.
50
+ workflow: Workflow filename (e.g. ``release.yml``).
51
+ environment: GitHub Actions environment name (e.g. ``pypi``).
52
+
53
+ Returns:
54
+ Dict matching the PyPI ``_/v1/publisher`` schema.
55
+ """
56
+ # The schema uses `repository_owner` for the GitHub login (org or user).
57
+ # If `owner` happens to contain a `/`, take the first segment — that's
58
+ # the GitHub login the rest of the payload already uses.
59
+ repo_owner = owner.split("/", 1)[0] if "/" in owner else owner
60
+ return {
61
+ "name": normalise_project_name(project),
62
+ "owner": repo_owner,
63
+ "repository": repo,
64
+ "workflow_filename": workflow,
65
+ "environment": environment,
66
+ }
67
+
68
+
69
+ def add_pending_publisher(
70
+ api_token: str,
71
+ payload: Dict[str, Any],
72
+ *,
73
+ timeout: float = 15.0,
74
+ base_url: str = PYPI_API,
75
+ ) -> Tuple[bool, str]:
76
+ """POST a pending-publisher registration to PyPI.
77
+
78
+ Args:
79
+ api_token: PyPI API token (format: ``pypi-...``). NOT a password.
80
+ payload: The dict produced by :func:`get_pending_publisher_payload`.
81
+ timeout: HTTP timeout in seconds.
82
+ base_url: Override the PyPI base URL (for test fixtures).
83
+
84
+ Returns:
85
+ Tuple ``(success, message)``. ``success`` is ``True`` only when
86
+ PyPI returns HTTP 201. The ``message`` is a human-readable
87
+ description suitable for printing to a terminal.
88
+ """
89
+ url = f"{base_url}/_/v1/publisher"
90
+ body = json.dumps(payload).encode("utf-8")
91
+ req = urllib.request.Request(
92
+ url,
93
+ data=body,
94
+ headers={
95
+ "Content-Type": "application/json",
96
+ "Authorization": f"Token {api_token}",
97
+ },
98
+ method="POST",
99
+ )
100
+ try:
101
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
102
+ status = resp.status
103
+ text = resp.read().decode("utf-8", errors="replace")
104
+ if status == 201:
105
+ return True, (
106
+ "Pending publisher created. PyPI emailed the maintainer "
107
+ "at the account email. Click the magic link to confirm."
108
+ )
109
+ return False, f"PyPI returned HTTP {status}: {text}"
110
+ except urllib.error.HTTPError as e:
111
+ # The error body often carries machine-readable details. We surface
112
+ # both the status and the body so the maintainer can diagnose
113
+ # without re-running the request.
114
+ err_body = e.read().decode("utf-8", errors="replace")
115
+ return False, f"HTTP {e.code}: {err_body}"
116
+ except urllib.error.URLError as e:
117
+ return False, f"Network error: {e.reason}"
118
+ except TimeoutError:
119
+ return False, f"Request timed out after {timeout}s."
120
+ except Exception as e: # pragma: no cover — defensive
121
+ return False, f"Unexpected error: {e}"
122
+
123
+
124
+ def build_argparser() -> argparse.ArgumentParser:
125
+ """Construct the CLI argument parser.
126
+
127
+ Kept as a separate function so tests can introspect / invoke it
128
+ without spawning the full ``main()`` flow.
129
+ """
130
+ parser = argparse.ArgumentParser(
131
+ prog="python -m sin_code_bundle.tools.pypi_setup",
132
+ description=(
133
+ "One-click PyPI Trusted Publisher setup for "
134
+ "OpenSIN-Code/SIN-Code-Bundle (tokenless OIDC publishing)."
135
+ ),
136
+ )
137
+ parser.add_argument(
138
+ "--project",
139
+ default="sin-code-bundle",
140
+ help="PyPI project name (default: %(default)s)",
141
+ )
142
+ parser.add_argument(
143
+ "--owner",
144
+ default="OpenSIN-Code",
145
+ help="GitHub owner/org (default: %(default)s)",
146
+ )
147
+ parser.add_argument(
148
+ "--repo",
149
+ default="SIN-Code-Bundle",
150
+ help="GitHub repository name (default: %(default)s)",
151
+ )
152
+ parser.add_argument(
153
+ "--workflow",
154
+ default="release.yml",
155
+ help="GitHub Actions workflow filename (default: %(default)s)",
156
+ )
157
+ parser.add_argument(
158
+ "--environment",
159
+ default="pypi",
160
+ help="GitHub Actions environment name (default: %(default)s)",
161
+ )
162
+ parser.add_argument(
163
+ "--api-token",
164
+ required=True,
165
+ help="PyPI API token (format: pypi-...). NOT a password.",
166
+ )
167
+ parser.add_argument(
168
+ "--timeout",
169
+ type=float,
170
+ default=15.0,
171
+ help="HTTP timeout in seconds (default: %(default)s)",
172
+ )
173
+ parser.add_argument(
174
+ "--dry-run",
175
+ action="store_true",
176
+ help="Print the payload that would be sent, then exit 0.",
177
+ )
178
+ parser.add_argument(
179
+ "--json",
180
+ dest="as_json",
181
+ action="store_true",
182
+ help="Emit the result as a single JSON line (for piping).",
183
+ )
184
+ return parser
185
+
186
+
187
+ def _print_human(
188
+ project: str,
189
+ owner: str,
190
+ repo: str,
191
+ workflow: str,
192
+ environment: str,
193
+ payload: Dict[str, Any],
194
+ result: Tuple[bool, str],
195
+ ) -> None:
196
+ """Pretty-print the result to stdout/stderr for human operators."""
197
+ out = sys.stdout
198
+ print("=== PyPI Trusted Publisher setup ===", file=out)
199
+ print(f"Project: {project}", file=out)
200
+ print(f"Owner: {owner}", file=out)
201
+ print(f"Repository: {repo}", file=out)
202
+ print(f"Workflow: {workflow}", file=out)
203
+ print(f"Environment: {environment}", file=out)
204
+ print("", file=out)
205
+ print("Payload:", file=out)
206
+ print(json.dumps(payload, indent=2), file=out)
207
+ print("", file=out)
208
+
209
+ success, message = result
210
+ if success:
211
+ print(f"OK {message}", file=out)
212
+ print("", file=out)
213
+ print("Next steps:", file=out)
214
+ print(" 1. Check the email registered on the PyPI account.", file=out)
215
+ print(" 2. Click the magic link PyPI sent.", file=out)
216
+ print(
217
+ " 3. From now on, every `git tag v*.*.* && git push origin v*.*.*` in",
218
+ file=out,
219
+ )
220
+ print(
221
+ f" {owner}/{repo} auto-publishes to PyPI in ~30s.",
222
+ file=out,
223
+ )
224
+ else:
225
+ print(f"FAIL {message}", file=sys.stderr)
226
+ print("", file=sys.stderr)
227
+ print("Manual fallback:", file=sys.stderr)
228
+ print(f" 1. Open {FALLBACK_URL}", file=sys.stderr)
229
+ print(" 2. Click 'Add a new pending publisher'.", file=sys.stderr)
230
+ print(f" 3. Project name: {project}", file=sys.stderr)
231
+ print(f" 4. Owner: {owner}", file=sys.stderr)
232
+ print(f" 5. Repository name: {repo}", file=sys.stderr)
233
+ print(f" 6. Workflow filename: {workflow}", file=sys.stderr)
234
+ print(f" 7. Environment name: {environment}", file=sys.stderr)
235
+
236
+
237
+ def main(argv: Optional[list] = None) -> int:
238
+ """CLI entry point.
239
+
240
+ Args:
241
+ argv: Optional argument list (defaults to ``sys.argv[1:]``).
242
+
243
+ Returns:
244
+ Process exit code — ``0`` on success, ``1`` on any failure.
245
+ """
246
+ args = build_argparser().parse_args(argv)
247
+ payload = get_pending_publisher_payload(
248
+ args.project,
249
+ args.owner,
250
+ args.repo,
251
+ args.workflow,
252
+ args.environment,
253
+ )
254
+
255
+ if args.dry_run:
256
+ if args.as_json:
257
+ print(json.dumps({"dry_run": True, "payload": payload}))
258
+ else:
259
+ print(json.dumps(payload, indent=2))
260
+ return 0
261
+
262
+ result = add_pending_publisher(args.api_token, payload, timeout=args.timeout)
263
+
264
+ if args.as_json:
265
+ print(
266
+ json.dumps(
267
+ {
268
+ "success": result[0],
269
+ "message": result[1],
270
+ "payload": payload,
271
+ }
272
+ )
273
+ )
274
+ else:
275
+ _print_human(
276
+ args.project,
277
+ args.owner,
278
+ args.repo,
279
+ args.workflow,
280
+ args.environment,
281
+ payload,
282
+ result,
283
+ )
284
+
285
+ return 0 if result[0] else 1
286
+
287
+
288
+ if __name__ == "__main__": # pragma: no cover
289
+ sys.exit(main())
sin_code_bundle/vfs.py ADDED
@@ -0,0 +1,264 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Purpose: Virtual Filesystem Layer (URI Schemes) for SIN-Code v2.
3
+
4
+ Docs: vfs.doc.md
5
+
6
+ Exposes semantic tools as URI schemes for any MCP client:
7
+ sckg://module/<name>/dependencies -> SCKG graph query
8
+ sckg://module/<name>/callers -> SCKG reverse lookup
9
+ sckg://module/<name>/neighbors -> SCKG neighbors
10
+ poc://strategy/<name> -> POC strategy list
11
+ ibd://diff/<file> -> IBD parse file
12
+ adw://smell/<name> -> ADW smell analyzer
13
+ efsm://service/<name> -> EFSM mock service
14
+ oracle://strategy/<name> -> Oracle verifier
15
+ conflict://<id> -> Git conflict interface
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ import subprocess
22
+ from pathlib import Path
23
+ from typing import Any, Dict, Optional
24
+
25
+ URI_SCHEMES = {
26
+ "sckg": "Semantic Codebase Knowledge Graph",
27
+ "poc": "Proof of Correctness",
28
+ "ibd": "Intent-Based Semantic Diff",
29
+ "adw": "Architectural Debt Watchdog",
30
+ "efsm": "Ephemeral Full-Stack Mock",
31
+ "oracle": "Verification Oracle",
32
+ "conflict": "Merge Conflict Resolution",
33
+ }
34
+
35
+
36
+ # ── SINVirtualFS: URI Dispatcher + Resolvers ───────────────────────────────
37
+ class SINVirtualFS:
38
+ """Resolves SIN-specific URI schemes.
39
+
40
+ Usage:
41
+ vfs = SINVirtualFS(Path("/path/to/repo"))
42
+ result = vfs.resolve("sckg://module/auth/dependencies")
43
+ """
44
+
45
+ def __init__(self, repo_root: Optional[Path] = None):
46
+ self.repo_root = repo_root or Path.cwd()
47
+ # _cache avoids recomputing expensive SCKG queries on repeated
48
+ # resolve() calls (e.g. when an agent re-reads the same module
49
+ # graph mid-session). Bounded only by session lifetime — fine
50
+ # for our short-lived agent processes.
51
+ self._cache: Dict[str, Any] = {}
52
+
53
+ def resolve(self, uri: str) -> Dict[str, Any]:
54
+ """Resolve a SIN URI to structured content."""
55
+ # URI grammar per RFC 3986-ish: `scheme://path`. \w+ matches
56
+ # `[A-Za-z0-9_]+` which covers all our scheme names (sckg, poc,
57
+ # ibd, adw, efsm, oracle, conflict) and rejects whitespace,
58
+ # colons, and other URI-illegal chars early.
59
+ match = re.match(r"^(\w+)://(.+)$", uri)
60
+ if not match:
61
+ return {"error": f"Invalid URI format: {uri}"}
62
+ scheme, path = match.group(1), match.group(2)
63
+
64
+ cache_key = f"{scheme}://{path}"
65
+ if cache_key in self._cache:
66
+ return self._cache[cache_key]
67
+
68
+ handler = getattr(self, f"_resolve_{scheme}", None)
69
+ if not handler:
70
+ return {"error": f"Unknown scheme: {scheme}"}
71
+ result = handler(path)
72
+ self._cache[cache_key] = result
73
+ return result
74
+
75
+ def list_schemes(self) -> Dict[str, str]:
76
+ """List all available URI schemes."""
77
+ return dict(URI_SCHEMES)
78
+
79
+ # ── SCKG resolver (uses REAL KnowledgeGraph API) ─────────────────
80
+ def _resolve_sckg(self, path: str) -> Dict[str, Any]:
81
+ parts = path.split("/")
82
+ if len(parts) < 2 or parts[0] != "module":
83
+ return {"error": "Use sckg://module/<name>/<query_type>"}
84
+ module_name = parts[1]
85
+ query_type = parts[2] if len(parts) > 2 else "neighbors"
86
+ # try/except around every resolver = graceful degradation:
87
+ # if one subsystem breaks or is missing, the others still resolve.
88
+ try:
89
+ from sin_code_sckg import KnowledgeGraph
90
+
91
+ kg = KnowledgeGraph(str(self.repo_root))
92
+ kg.build_from_repo()
93
+ node_id = "module:" + module_name
94
+ result_data = {
95
+ "module": module_name,
96
+ "query_type": query_type,
97
+ }
98
+ if query_type == "neighbors":
99
+ result_data["data"] = [str(n) for n in kg.get_neighbors(node_id)]
100
+ elif query_type == "overview":
101
+ result_data["data"] = kg.to_dict()
102
+ else:
103
+ result_data["data"] = kg.query(
104
+ f"MATCH (n:Module) WHERE n.name='{module_name}' RETURN n"
105
+ )
106
+ return {"type": "sckg_module", **result_data}
107
+ except ImportError:
108
+ return {"error": "SCKG not installed (pip install sin-code-sckg)"}
109
+ except Exception as e:
110
+ return {"error": f"SCKG error: {e}"}
111
+
112
+ # ── POC resolver (uses REAL POC API) ─────────────────────────────
113
+ def _resolve_poc(self, path: str) -> Dict[str, Any]:
114
+ parts = path.split("/")
115
+ if len(parts) < 2:
116
+ return {"error": "Use poc://strategy/<name>"}
117
+ strategy_name = parts[1]
118
+ # try/except around every resolver = graceful degradation:
119
+ # if one subsystem breaks or is missing, the others still resolve.
120
+ try:
121
+ from sin_code_poc import list_properties, property_metadata # noqa: F401
122
+
123
+ # Use the property registry for strategy listing
124
+ props = property_metadata() if callable(property_metadata) else {}
125
+ # [:50] limits blast radius — not the whole catalog — so the
126
+ # response stays LLM-prompt-friendly (POC has hundreds of
127
+ # properties, we only need a discoverable subset here).
128
+ return {
129
+ "type": "poc_strategy",
130
+ "strategy": strategy_name,
131
+ "available_properties": list(props.keys())[:50] if isinstance(props, dict) else [],
132
+ "note": f"Run: sin poc verify --strategy={strategy_name} <file>",
133
+ }
134
+ except ImportError:
135
+ return {"error": "POC not installed"}
136
+
137
+ # ── IBD resolver (uses REAL IBD API) ─────────────────────────────
138
+ def _resolve_ibd(self, path: str) -> Dict[str, Any]:
139
+ parts = path.split("/")
140
+ if len(parts) < 2 or parts[0] != "diff":
141
+ return {"error": "Use ibd://diff/<file_path>"}
142
+ file_path = self.repo_root / parts[1]
143
+ if not file_path.exists():
144
+ return {"error": f"File not found: {file_path}"}
145
+ try:
146
+ from sin_code_ibd import ASTDiff
147
+
148
+ diff = ASTDiff(str(file_path))
149
+ return {
150
+ "type": "ibd_diff",
151
+ "file": str(file_path),
152
+ "ast": diff.to_dict() if hasattr(diff, "to_dict") else str(diff),
153
+ }
154
+ except ImportError:
155
+ return {"error": "IBD not installed"}
156
+
157
+ # ── ADW resolver (uses REAL ADW API) ─────────────────────────────
158
+ def _resolve_adw(self, path: str) -> Dict[str, Any]:
159
+ parts = path.split("/")
160
+ if len(parts) < 2 or parts[0] != "smell":
161
+ return {"error": "Use adw://smell/<name>"}
162
+ smell_name = parts[1]
163
+ # try/except around every resolver = graceful degradation:
164
+ # if one subsystem breaks or is missing, the others still resolve.
165
+ try:
166
+ from sin_code_adw import smells
167
+
168
+ # ADW doesn't expose a unified query API, so we surface the
169
+ # available analyzers and tell the user to call them directly
170
+ # (same rationale as EFSM/Oracle/POC below).
171
+ available = [
172
+ m for m in dir(smells) if not m.startswith("_") and callable(getattr(smells, m))
173
+ ]
174
+ return {
175
+ "type": "adw_smell",
176
+ "name": smell_name,
177
+ "available_analyzers": available,
178
+ }
179
+ except ImportError:
180
+ return {"error": "ADW not installed"}
181
+
182
+ # ── EFSM resolver (uses REAL EFSM API) ───────────────────────────
183
+ def _resolve_efsm(self, path: str) -> Dict[str, Any]:
184
+ parts = path.split("/")
185
+ if len(parts) < 2 or parts[0] != "service":
186
+ return {"error": "Use efsm://service/<name>"}
187
+ service_name = parts[1]
188
+ # try/except around every resolver = graceful degradation:
189
+ # if one subsystem breaks or is missing, the others still resolve.
190
+ try:
191
+ from sin_code_efsm import services
192
+
193
+ # EFSM doesn't expose a unified query API, so we surface the
194
+ # available services and tell the user to call them directly
195
+ # (same rationale as ADW/Oracle/POC above).
196
+ available = [m for m in dir(services) if not m.startswith("_")]
197
+ return {
198
+ "type": "efsm_service",
199
+ "name": service_name,
200
+ "available": available,
201
+ "note": f"Run: sin efsm create --service={service_name}",
202
+ }
203
+ except ImportError:
204
+ return {"error": "EFSM not installed"}
205
+
206
+ # ── Oracle resolver (uses REAL Oracle API) ───────────────────────
207
+ def _resolve_oracle(self, path: str) -> Dict[str, Any]:
208
+ parts = path.split("/")
209
+ if len(parts) < 2 or parts[0] != "strategy":
210
+ return {"error": "Use oracle://strategy/<name>"}
211
+ strategy_name = parts[1]
212
+ # try/except around every resolver = graceful degradation:
213
+ # if one subsystem breaks or is missing, the others still resolve.
214
+ try:
215
+ from sin_code_oracle import verifier
216
+
217
+ available = [
218
+ m for m in dir(verifier) if not m.startswith("_") and callable(getattr(verifier, m))
219
+ ]
220
+ # [:20] limits blast radius — Oracle's verifier module can
221
+ # have many callables; we just need enough to be discoverable.
222
+ # Oracle doesn't expose a unified query API either, so we
223
+ # return the available verifiers and let the user call them
224
+ # directly (same rationale as ADW/EFSM/POC above).
225
+ return {
226
+ "type": "oracle_strategy",
227
+ "strategy": strategy_name,
228
+ "available": available[:20],
229
+ }
230
+ except ImportError:
231
+ return {"error": "Oracle not installed"}
232
+
233
+ # ── Conflict resolver (git-based) ────────────────────────────────
234
+ def _resolve_conflict(self, path: str) -> Dict[str, Any]:
235
+ try:
236
+ # git-based and cheap: `git diff --name-only --diff-filter=U`
237
+ # lists unmerged (U-status) paths. We don't need to parse
238
+ # conflict markers ourselves — just surface the file list.
239
+ # 10s timeout — `git diff` is in-memory; only a broken index would
240
+ # make it slow, and we want to fail fast in that case.
241
+ result = subprocess.run(
242
+ ["git", "diff", "--name-only", "--diff-filter=U"],
243
+ capture_output=True,
244
+ text=True,
245
+ cwd=self.repo_root,
246
+ timeout=10,
247
+ )
248
+ conflicted = [f for f in result.stdout.splitlines() if f]
249
+ except Exception as e:
250
+ return {"error": f"git failed: {e}"}
251
+
252
+ if path == "*" or path == "":
253
+ # conflict://* = bulk list (most common, agents usually want
254
+ # "what files are in conflict?" not a single one).
255
+ return {"type": "conflict_bulk", "files": conflicted, "count": len(conflicted)}
256
+ if path.isdigit():
257
+ idx = int(path)
258
+ if 0 <= idx < len(conflicted):
259
+ return {"type": "conflict_single", "file": conflicted[idx]}
260
+ return {"error": f"Conflict index {idx} out of range (have {len(conflicted)})"}
261
+ return {"error": "Use conflict://* for all or conflict://<N> for specific"}
262
+
263
+
264
+ __all__ = ["SINVirtualFS", "URI_SCHEMES"]