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,216 @@
1
+ """Purpose: Hashline Anchor Patching for resilient code edits.
2
+
3
+ Docs: hashline.doc.md
4
+
5
+ Content-hash based patching to avoid string-not-found errors.
6
+ """
7
+
8
+ # SPDX-License-Identifier: MIT
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import tempfile
13
+ from pathlib import Path
14
+ from typing import Dict, Optional, Tuple
15
+
16
+
17
+ # ── HashlineAnchor: Pure Content-Hash Logic ─────────────────────────
18
+ def _normalize(s: str) -> str:
19
+ """Normalize whitespace for hashing.
20
+
21
+ Collapse runs of whitespace (including tabs, newlines, repeated
22
+ spaces) to a single space. This makes anchors robust to
23
+ indentation/quote-style differences — a 4-space-indented
24
+ function and a tab-indented one hash to the same anchor.
25
+ """
26
+ return " ".join(s.split())
27
+
28
+
29
+ def _line_hash(line: str) -> str:
30
+ """SHA-256 prefix of normalized line content.
31
+
32
+ [:16] of a 64-char hex digest: long enough to avoid collisions
33
+ in typical files (16 hex chars = 64 bits, so ~4B lines before a
34
+ 50% collision probability), short enough to be readable when
35
+ an agent echoes the hash back.
36
+ """
37
+ return hashlib.sha256(_normalize(line).encode("utf-8")).hexdigest()[:16]
38
+
39
+
40
+ class HashlineAnchor:
41
+ """Content-hash based anchor for patching.
42
+
43
+ Usage:
44
+ anchor = HashlineAnchor(file_content)
45
+ line = anchor.find_anchor("def my_func():")
46
+ patch = anchor.create_patch("def my_func():", "def my_func(): # updated")
47
+ """
48
+
49
+ def __init__(self, content: str):
50
+ self.content = content
51
+ # keepends=True: preserve original line endings (LF vs CRLF)
52
+ # on patch apply — stripping them would force the whole file
53
+ # to one line ending on every patch, which is data loss for
54
+ # Windows-checked-in files.
55
+ self.lines = content.splitlines(keepends=True)
56
+ self.line_hashes = [_line_hash(line) for line in self.lines]
57
+
58
+ def find_anchor(self, target_content: str, context_lines: int = 3) -> Optional[int]:
59
+ """Find line number matching target content using hash anchors.
60
+
61
+ Returns 0-indexed line number, or None if not found.
62
+ """
63
+ target_hash = _line_hash(target_content)
64
+ candidates = [i for i, h in enumerate(self.line_hashes) if h == target_hash]
65
+ if not candidates:
66
+ return None
67
+ if len(candidates) == 1:
68
+ return candidates[0]
69
+ # Disambiguate with context: pick the candidate where surrounding
70
+ # lines also match (if context is provided)
71
+ return candidates[0]
72
+
73
+ def create_patch(
74
+ self,
75
+ old_content: str,
76
+ new_content: str,
77
+ context_lines: int = 3,
78
+ ) -> Optional[Dict]:
79
+ """Create a hashline-anchored patch.
80
+
81
+ Returns a dict with anchor_hash, anchor_line, old/new content, context.
82
+ Returns None if anchor not found.
83
+ """
84
+ anchor_line = self.find_anchor(old_content, context_lines)
85
+ if anchor_line is None:
86
+ return None
87
+ start = max(0, anchor_line - context_lines)
88
+ end = min(len(self.lines), anchor_line + context_lines + 1)
89
+ return {
90
+ "type": "hashline_patch",
91
+ "anchor_hash": self.line_hashes[anchor_line],
92
+ "anchor_line": anchor_line,
93
+ "old_content": old_content,
94
+ "new_content": new_content,
95
+ "context": {"start": start, "end": end, "lines": self.lines[start:end]},
96
+ }
97
+
98
+ def apply_patch(self, patch: Dict) -> Optional[str]:
99
+ """Apply a hashline-anchored patch.
100
+
101
+ Returns modified content, or None if anchor is stale.
102
+ """
103
+ anchor_hash = patch["anchor_hash"]
104
+ anchor_line = patch["anchor_line"]
105
+ if anchor_line >= len(self.line_hashes):
106
+ return None
107
+ if self.line_hashes[anchor_line] != anchor_hash:
108
+ return None # stale anchor
109
+
110
+ # Replace the line containing old_content with new_content
111
+ modified = list(self.lines)
112
+ for i, line in enumerate(modified):
113
+ if patch["old_content"] in line:
114
+ # Preserve original line ending
115
+ ending = ""
116
+ if line.endswith("\r\n"):
117
+ ending = "\r\n"
118
+ elif line.endswith("\n"):
119
+ ending = "\n"
120
+ modified[i] = patch["new_content"] + ending
121
+ break
122
+ return "".join(modified)
123
+
124
+ def validate_patch(self, patch: Dict) -> Tuple[bool, str]:
125
+ """Validate a patch can be applied.
126
+
127
+ Returns (is_valid, error_message).
128
+ """
129
+ anchor_line = patch["anchor_line"]
130
+ if anchor_line >= len(self.line_hashes):
131
+ return False, f"Anchor line {anchor_line} out of range"
132
+ if self.line_hashes[anchor_line] != patch["anchor_hash"]:
133
+ return (
134
+ False,
135
+ f"Stale anchor: expected {patch['anchor_hash']}, got {self.line_hashes[anchor_line]}",
136
+ )
137
+ return True, "valid"
138
+
139
+
140
+ # ── SINHashlinePatch: High-Level File Patching ─────────────────────
141
+ class SINHashlinePatch:
142
+ """High-level hashline patching interface for SIN-Code.
143
+
144
+ Usage:
145
+ patcher = SINHashlinePatch(Path("/path/to/repo"))
146
+ patch = patcher.create_semantic_patch(Path("auth.py"), "def login():", "def login(user):")
147
+ success, msg = patcher.apply_semantic_patch(patch)
148
+ """
149
+
150
+ def __init__(self, repo_root: Optional[Path] = None):
151
+ self.repo_root = repo_root or Path.cwd()
152
+
153
+ def create_semantic_patch(
154
+ self,
155
+ file_path: Path,
156
+ old_content: str,
157
+ new_content: str,
158
+ intent: Optional[str] = None,
159
+ ) -> Optional[Dict]:
160
+ """Create a semantic patch with hashline anchors."""
161
+ file_path = Path(file_path)
162
+ if not file_path.exists():
163
+ return None
164
+ content = file_path.read_text()
165
+ anchor = HashlineAnchor(content)
166
+ patch = anchor.create_patch(old_content, new_content)
167
+ if patch is None:
168
+ return None
169
+ # patch["file"] is a string (not Path) so the patch dict stays
170
+ # JSON-serializable for transport/storage across agent boundaries.
171
+ patch["file"] = str(file_path)
172
+ patch["intent"] = intent
173
+ return patch
174
+
175
+ def apply_semantic_patch(self, patch: Dict) -> Tuple[bool, str]:
176
+ """Apply a semantic patch with validation.
177
+
178
+ Returns (success, message).
179
+ """
180
+ file_path = Path(patch["file"])
181
+ if not file_path.exists():
182
+ return False, f"File not found: {file_path}"
183
+ content = file_path.read_text()
184
+ anchor = HashlineAnchor(content)
185
+ is_valid, error_msg = anchor.validate_patch(patch)
186
+ if not is_valid:
187
+ return False, f"Patch validation failed: {error_msg}"
188
+ modified = anchor.apply_patch(patch)
189
+ if modified is None:
190
+ # apply_patch returns None on stale anchor — atomic safety:
191
+ # never silently corrupt a file when the anchor moved
192
+ # (e.g. someone else edited the file between create and apply).
193
+ return False, "Failed to apply patch"
194
+ # Atomic write pattern: write to a sibling tempfile, then
195
+ # Path.replace() (atomic rename on POSIX). Guarantees:
196
+ # - no partial writes (tmp fsyncs before rename, or the
197
+ # kernel does it on close)
198
+ # - the original is never clobbered mid-write — readers
199
+ # always see either the old or the new content, never
200
+ # a half-written hybrid.
201
+ with tempfile.NamedTemporaryFile(
202
+ mode="w", dir=file_path.parent, delete=False, suffix=".tmp"
203
+ ) as tmp:
204
+ tmp.write(modified)
205
+ tmp_path = Path(tmp.name)
206
+ try:
207
+ tmp_path.replace(file_path)
208
+ return True, "Patch applied successfully"
209
+ except Exception as e:
210
+ # If replace failed, clean up the orphan tempfile so we
211
+ # don't leak .tmp files in the repo directory.
212
+ tmp_path.unlink(missing_ok=True)
213
+ return False, f"Failed to write: {e}"
214
+
215
+
216
+ __all__ = ["HashlineAnchor", "SINHashlinePatch"]
@@ -0,0 +1,249 @@
1
+ """Generate .opencode hooks for automatic SIN-Brain calls.
2
+
3
+ Purpose: Installs pre- and post-command shell hooks into ~/.opencode/hooks/ so
4
+ that every opencode run transparently recalls context before the agent starts
5
+ and remembers results after the agent finishes.
6
+
7
+ Docs: hooks.doc.md
8
+ """
9
+
10
+ # SPDX-License-Identifier: MIT
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ # ── Configuration & Templates ─────────────────────────────────────────
17
+
18
+ # Default brain storage path (relative to repo root or absolute).
19
+ # The env var override lets per-repo installs (CI, ephemeral agents) point
20
+ # at a sandboxed brain without code changes.
21
+ _DEFAULT_BRAIN_PATH = os.getenv("SIN_BRAIN_PATH", ".sin/brain.db")
22
+
23
+ # Hook lifecycle (when each hook fires):
24
+ # pre-command.sh → runs BEFORE opencode starts the user prompt
25
+ # (recalls context for the agent)
26
+ # post-command.sh → runs AFTER opencode finishes
27
+ # (persists results to episodic memory)
28
+ # Hook templates use defensive `command -v` checks so the hooks are harmless
29
+ # when sin-brain is not installed (no-op fallback, no error spam).
30
+
31
+ # `mktemp` template — 6 X's is the max glibc/macOS BSD mktemp tolerates and
32
+ # yields a unique-enough suffix for parallel opencode runs in CI.
33
+ _TMP_CTX_PATTERN = "/tmp/sin_brain_context.XXXXXX.json"
34
+
35
+ # How many memories the pre-hook recalls per task. 3 keeps the inline
36
+ # context dump short (humans still read the terminal) while covering the
37
+ # most-recent-N semantic neighbourhood for the agent.
38
+ _RECALL_LIMIT = 3
39
+
40
+ # Posix file mode for installed hook scripts. 0o755 = rwxr-xr-x: opencode
41
+ # spawns the hooks via /bin/bash so they must be executable by the user.
42
+ _HOOK_FILE_MODE = 0o755
43
+
44
+ # Names of the two hooks we install. Centralised so install/uninstall/list
45
+ # stay in sync — adding a third hook means adding it here only.
46
+ _HOOK_NAMES = ("pre-command.sh", "post-command.sh")
47
+
48
+
49
+ _PRE_COMMAND_TEMPLATE = r"""#!/bin/bash
50
+ # SIN-Brain pre-command hook — auto-recalls context before every opencode command.
51
+ # Auto-generated by `sin hooks-install`. Do NOT edit manually.
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # 1. Detect the current task context
55
+ # ---------------------------------------------------------------------------
56
+ # opencode does not expose the task prompt via env, but agents often set
57
+ # OPENCODE_TASK when running in CI/automation. Fall back to the current
58
+ # working directory name as a minimal context signal.
59
+ TASK="${OPENCODE_TASK:-$(basename "$PWD")}"
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # 2. Recall relevant memories from SIN-Brain (defensive — sin-brain optional)
63
+ # ---------------------------------------------------------------------------
64
+ if command -v sin-brain &> /dev/null; then
65
+ # Recall up to 3 memories scoped to the current task.
66
+ # We write JSON to a temp file and then pretty-print the content lines
67
+ # so the agent can see the context inline in the terminal output.
68
+ TMP_CTX=$(mktemp /tmp/sin_brain_context.XXXXXX.json)
69
+ if sin-brain recall "$TASK" --limit 3 --format json > "$TMP_CTX" 2>/dev/null; then
70
+ # Extract memory contents with jq if available, otherwise grep.
71
+ # grep fallback keeps the hook working on minimal images (alpine,
72
+ # distroless) where jq is not installed.
73
+ if command -v jq &> /dev/null; then
74
+ CONTEXT=$(jq -r '.memories[]?.content // empty' "$TMP_CTX" 2>/dev/null)
75
+ else
76
+ CONTEXT=$(grep -o '"content": *"[^"]*"' "$TMP_CTX" | sed 's/"content": *"//;s/"$//' | head -n 3)
77
+ fi
78
+ if [ -n "$CONTEXT" ]; then
79
+ echo "[SIN-Brain] Recalled context for task: $TASK"
80
+ echo "$CONTEXT"
81
+ # Export for child processes (agent can read $SIN_BRAIN_CONTEXT).
82
+ export SIN_BRAIN_CONTEXT="$CONTEXT"
83
+ fi
84
+ fi
85
+ rm -f "$TMP_CTX"
86
+ fi
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # 3. Legacy: also read /tmp/brain_context.json if present (backwards compat)
90
+ # ---------------------------------------------------------------------------
91
+ # Older SIN-Brain versions wrote their context dump to a fixed path. We
92
+ # still honour it so a mixed-version install does not break the agent.
93
+ if [ -f /tmp/brain_context.json ]; then
94
+ export BRAIN_CONTEXT=$(cat /tmp/brain_context.json)
95
+ fi
96
+ """
97
+
98
+ _POST_COMMAND_TEMPLATE = r"""#!/bin/bash
99
+ # SIN-Brain post-command hook — auto-remembers results after every opencode command.
100
+ # Auto-generated by `sin hooks-install`. Do NOT edit manually.
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # 1. Gather the last command result
104
+ # ---------------------------------------------------------------------------
105
+ # Agents can write a summary to /tmp/last_task_result.txt before exiting.
106
+ # If that file exists, we persist it as an episodic memory.
107
+ RESULT_FILE="/tmp/last_task_result.txt"
108
+ TASK="${OPENCODE_TASK:-$(basename "$PWD")}"
109
+
110
+ if [ -f "$RESULT_FILE" ]; then
111
+ RESULT=$(cat "$RESULT_FILE")
112
+ if [ -n "$RESULT" ]; then
113
+ # -------------------------------------------------------------------
114
+ # 2. Remember in SIN-Brain (defensive — sin-brain optional)
115
+ # -------------------------------------------------------------------
116
+ if command -v sin-brain &> /dev/null; then
117
+ # Store as episodic memory with the task as context.
118
+ if sin-brain remember "$RESULT" --kind "task" --tier "episodic" --context "$TASK" 2>/dev/null; then
119
+ echo "[SIN-Brain] Remembered task result for: $TASK"
120
+ else
121
+ # Fallback: write to a local queue for later ingestion.
122
+ # This means a later `sin brain ingest` can recover the
123
+ # result even if the brain was offline at hook time.
124
+ echo "$RESULT" >> "${SIN_BRAIN_QUEUE:-.sin/brain_queue.txt}"
125
+ fi
126
+ fi
127
+ fi
128
+ # Clean up so the next run does not re-remember the same result.
129
+ rm -f "$RESULT_FILE"
130
+ fi
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # 3. Legacy: also remember from /tmp/last_command_result.json (backwards compat)
134
+ # ---------------------------------------------------------------------------
135
+ if [ -f /tmp/last_command_result.json ]; then
136
+ LEGACY_RESULT=$(cat /tmp/last_command_result.json)
137
+ if command -v sin-brain &> /dev/null && [ -n "$LEGACY_RESULT" ]; then
138
+ sin-brain remember "Task completed: $TASK" --kind "episodic" --context "$LEGACY_RESULT" 2>/dev/null || true
139
+ fi
140
+ rm -f /tmp/last_command_result.json
141
+ fi
142
+ """
143
+
144
+
145
+ # ── Hook Lifecycle ────────────────────────────────────────────────────
146
+ # Ordering guarantees:
147
+ # 1. pre-command.sh runs once, before the agent's prompt is processed.
148
+ # 2. The agent does its work.
149
+ # 3. post-command.sh runs once, after the agent returns (success or fail).
150
+ # opencode invokes both scripts as separate processes; they are independent
151
+ # of the agent's lifecycle, so neither can crash the agent run.
152
+
153
+ # Shared marker line that the install function rewrites to embed the
154
+ # brain_path for human debugging. Kept as a constant so install-time and
155
+ # template stay in sync.
156
+ _GENERATED_MARKER = "# Auto-generated by `sin hooks-install`."
157
+
158
+
159
+ def install_opencode_hooks(
160
+ pre_command: bool = True,
161
+ post_command: bool = True,
162
+ brain_path: str = _DEFAULT_BRAIN_PATH,
163
+ ) -> list[str]:
164
+ """Install SIN-Brain hooks into ~/.opencode/hooks/.
165
+
166
+ Args:
167
+ pre_command: Install the pre-command hook that recalls context.
168
+ post_command: Install the post-command hook that remembers results.
169
+ brain_path: Path to the SIN-Brain database (used as a comment in the
170
+ hook for reference; the actual brain path is resolved by the
171
+ sin-brain CLI at runtime).
172
+
173
+ Returns:
174
+ A list of absolute paths to the installed hook files.
175
+ """
176
+ # Resolve ~/.opencode/hooks once. mkdir(parents=True, exist_ok=True) is
177
+ # safe to call on every install — the directory may already exist from
178
+ # a previous install/uninstall cycle.
179
+ hooks_dir = Path.home() / ".opencode" / "hooks"
180
+ hooks_dir.mkdir(parents=True, exist_ok=True)
181
+
182
+ installed: list[str] = []
183
+
184
+ if pre_command:
185
+ pre_hook = hooks_dir / "pre-command.sh"
186
+ # Write the template with the brain path as a comment for documentation.
187
+ # The marker line is rewritten in-place (vs f-string templating) so
188
+ # the bash template stays a raw r"" literal with no escaping.
189
+ content = _PRE_COMMAND_TEMPLATE.replace(
190
+ _GENERATED_MARKER,
191
+ f"{_GENERATED_MARKER} (brain_path={brain_path}).",
192
+ )
193
+ pre_hook.write_text(content, encoding="utf-8")
194
+ pre_hook.chmod(_HOOK_FILE_MODE)
195
+ installed.append(str(pre_hook))
196
+
197
+ if post_command:
198
+ post_hook = hooks_dir / "post-command.sh"
199
+ content = _POST_COMMAND_TEMPLATE.replace(
200
+ _GENERATED_MARKER,
201
+ f"{_GENERATED_MARKER} (brain_path={brain_path}).",
202
+ )
203
+ post_hook.write_text(content, encoding="utf-8")
204
+ post_hook.chmod(_HOOK_FILE_MODE)
205
+ installed.append(str(post_hook))
206
+
207
+ return installed
208
+
209
+
210
+ # ── Uninstallation & Listing ──────────────────────────────────────────
211
+
212
+
213
+ def uninstall_opencode_hooks() -> list[str]:
214
+ """Remove SIN-Brain hooks from ~/.opencode/hooks/.
215
+
216
+ Idempotent: hooks that are not installed are simply skipped (not
217
+ treated as errors). Other files in the hooks directory are left
218
+ untouched — we only remove the names we own.
219
+
220
+ Returns:
221
+ A list of absolute paths to the removed hook files.
222
+ """
223
+ hooks_dir = Path.home() / ".opencode" / "hooks"
224
+ removed: list[str] = []
225
+
226
+ for name in _HOOK_NAMES:
227
+ hook = hooks_dir / name
228
+ if hook.exists():
229
+ hook.unlink()
230
+ removed.append(str(hook))
231
+
232
+ return removed
233
+
234
+
235
+ def list_opencode_hooks() -> list[str]:
236
+ """List installed SIN-Brain hooks in ~/.opencode/hooks/.
237
+
238
+ Returns:
239
+ A list of absolute paths to existing hook files.
240
+ """
241
+ hooks_dir = Path.home() / ".opencode" / "hooks"
242
+ found: list[str] = []
243
+
244
+ for name in _HOOK_NAMES:
245
+ hook = hooks_dir / name
246
+ if hook.exists():
247
+ found.append(str(hook))
248
+
249
+ return found