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,208 @@
1
+ """SIN-Brain memory adapter (BR-1, Issue #14).
2
+
3
+ Thin, defensive bridge to the external ``sin_brain`` package. The bundle holds
4
+ **no** memory logic itself (that lives in SIN-Brain); this module only:
5
+
6
+ - detects whether ``sin_brain`` is importable and reports tier sizes for
7
+ ``sin status`` (:func:`detect_env`), and
8
+ - exposes the five memory operations (:func:`recall`, :func:`remember`,
9
+ :func:`forget`, :func:`pin`, :func:`link_evidence`) as thin pass-throughs that
10
+ the MCP ``serve`` command registers as tools.
11
+
12
+ Every entry point degrades gracefully: if ``sin_brain`` is absent, detection
13
+ reports ``available=False`` and the operations raise :class:`MemoryUnavailable`,
14
+ which the caller turns into a clean tool-level error instead of crashing the
15
+ server.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import importlib
21
+ import importlib.util
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+ PACKAGE = "sin_brain"
27
+
28
+ # Canonical enums (kept in lock-step with the plan + AGENTS.md guidance).
29
+ RECALL_SCOPES = ("recall", "archival", "graph")
30
+ REMEMBER_KINDS = ("decision", "convention", "fix", "pitfall", "preference")
31
+ REMEMBER_SCOPES = ("repo", "user")
32
+ EVIDENCE_SOURCES = ("oracle", "poc", "ibd", "sckg", "adw")
33
+
34
+
35
+ class MemoryUnavailable(RuntimeError):
36
+ """Raised when a memory operation is attempted without ``sin_brain``."""
37
+
38
+
39
+ @dataclass
40
+ class MemoryEnv:
41
+ """Runtime availability snapshot for ``sin status``."""
42
+
43
+ available: bool
44
+ db_path: str | None = None
45
+ tiers: dict[str, int] = field(default_factory=dict)
46
+ detail: str = ""
47
+
48
+ def to_dict(self) -> dict[str, Any]:
49
+ return {
50
+ "available": self.available,
51
+ "db_path": self.db_path,
52
+ "tiers": self.tiers,
53
+ "detail": self.detail,
54
+ }
55
+
56
+
57
+ def _tools_module():
58
+ """Import ``sin_brain.mcp_tools`` or raise :class:`MemoryUnavailable`."""
59
+ try:
60
+ return importlib.import_module(f"{PACKAGE}.mcp_tools")
61
+ except ImportError as exc: # pragma: no cover - exercised via detect_env
62
+ raise MemoryUnavailable(
63
+ "sin-brain not installed. Install with: pip install sin-brain"
64
+ ) from exc
65
+
66
+
67
+ def detect_env() -> MemoryEnv:
68
+ """Report whether SIN-Brain is installed and, if so, its tier sizes."""
69
+ if importlib.util.find_spec(PACKAGE) is None:
70
+ return MemoryEnv(available=False, detail="sin_brain package not importable")
71
+ try:
72
+ mod = importlib.import_module(PACKAGE)
73
+ except ImportError as exc: # pragma: no cover
74
+ return MemoryEnv(available=False, detail=f"import error: {exc}")
75
+
76
+ db_path = None
77
+ tiers: dict[str, int] = {}
78
+ # SIN-Brain exposes an optional, cheap introspection hook. Treat any failure
79
+ # as "available but stats unknown" rather than unavailable.
80
+ stats = getattr(mod, "stats", None)
81
+ if callable(stats):
82
+ try:
83
+ data = stats()
84
+ db_path = data.get("db_path")
85
+ tiers = data.get("tiers", {}) or {}
86
+ except Exception as exc: # noqa: BLE001 - never let stats break status
87
+ return MemoryEnv(available=True, detail=f"stats unavailable: {exc}")
88
+ return MemoryEnv(available=True, db_path=db_path, tiers=tiers, detail="ok")
89
+
90
+
91
+ # ── Operations — thin pass-throughs to sin_brain.mcp_tools (JSON-string results) ──
92
+ def recall(query: str, scope: str = "recall", k: int = 5) -> str:
93
+ """Tiered memory search. Returns JSON: ids + snippets (not full docs)."""
94
+ if scope not in RECALL_SCOPES:
95
+ raise ValueError(f"scope must be one of {RECALL_SCOPES}")
96
+ result = _tools_module().recall(query=query, scope=scope, k=k)
97
+ return result if isinstance(result, str) else json.dumps(result)
98
+
99
+
100
+ def remember(content: str, kind: str, ttl_days: int | None = None, scope: str = "repo") -> str:
101
+ """Self-editing memory write. Returns JSON with the new entry id."""
102
+ if kind not in REMEMBER_KINDS:
103
+ raise ValueError(f"kind must be one of {REMEMBER_KINDS}")
104
+ if scope not in REMEMBER_SCOPES:
105
+ raise ValueError(f"scope must be one of {REMEMBER_SCOPES}")
106
+ result = _tools_module().remember(content=content, kind=kind, ttl_days=ttl_days, scope=scope)
107
+ return result if isinstance(result, str) else json.dumps(result)
108
+
109
+
110
+ def forget(id: str) -> str:
111
+ """Remove a memory entry. Returns JSON status."""
112
+ result = _tools_module().forget(id=id)
113
+ return result if isinstance(result, str) else json.dumps(result)
114
+
115
+
116
+ def pin(id: str) -> str:
117
+ """Pin a memory entry so it is never evicted. Returns JSON status."""
118
+ result = _tools_module().pin(id=id)
119
+ return result if isinstance(result, str) else json.dumps(result)
120
+
121
+
122
+ def link_evidence(entity: str, verdict: str, source: str) -> str:
123
+ """Attach a subsystem verdict to a code entity in the evidence graph."""
124
+ if source not in EVIDENCE_SOURCES:
125
+ raise ValueError(f"source must be one of {EVIDENCE_SOURCES}")
126
+ result = _tools_module().link_evidence(entity=entity, verdict=verdict, source=source)
127
+ return result if isinstance(result, str) else json.dumps(result)
128
+
129
+
130
+ def inject() -> str:
131
+ """Return SIN-Brain's AGENTS.md inject block (SB-4), or '' if unavailable.
132
+
133
+ Used by `sin agents-md` to embed the project's compiled memory context. The
134
+ bundle owns no formatting here — SIN-Brain returns ready-to-embed Markdown.
135
+ """
136
+ if importlib.util.find_spec(PACKAGE) is None:
137
+ return ""
138
+ try:
139
+ mod = importlib.import_module(PACKAGE)
140
+ except ImportError:
141
+ return ""
142
+ fn = getattr(mod, "inject", None)
143
+ if not callable(fn):
144
+ return ""
145
+ try:
146
+ out = fn()
147
+ except Exception: # noqa: BLE001 - inject must never break callers
148
+ return ""
149
+ return out if isinstance(out, str) else ""
150
+
151
+
152
+ # ── MCP Registration (called by `sin serve`) ───────────────────────────────
153
+ # Kept here (not in cli.py) so the wiring is unit-testable with a fake MCP
154
+ # object and no `mcp` dependency.
155
+ TOOL_NAMES = ("recall", "remember", "forget", "pin", "link_evidence")
156
+
157
+
158
+ def register_tools(mcp: Any) -> list[str]:
159
+ """Register the five memory tools on ``mcp`` if SIN-Brain is available.
160
+
161
+ Returns the names registered (empty when sin-brain is absent) so callers and
162
+ tests can assert on the wiring. Never raises on a missing package — graceful
163
+ degradation is the contract.
164
+ """
165
+ if not detect_env().available:
166
+ return []
167
+
168
+ @mcp.tool()
169
+ def recall_tool(query: str, scope: str = "recall", k: int = 5) -> str:
170
+ """Search memory tiers (recall|archival|graph). Returns ids+snippets."""
171
+ try:
172
+ return recall(query, scope=scope, k=k)
173
+ except (MemoryUnavailable, ValueError) as exc:
174
+ return json.dumps({"error": str(exc)})
175
+
176
+ @mcp.tool()
177
+ def remember_tool(content: str, kind: str, ttl_days: int = 0, scope: str = "repo") -> str:
178
+ """Persist a memory. kind: decision|convention|fix|pitfall|preference."""
179
+ try:
180
+ return remember(content, kind, ttl_days=ttl_days or None, scope=scope)
181
+ except (MemoryUnavailable, ValueError) as exc:
182
+ return json.dumps({"error": str(exc)})
183
+
184
+ @mcp.tool()
185
+ def forget_tool(id: str) -> str:
186
+ """Delete a memory entry by id."""
187
+ try:
188
+ return forget(id)
189
+ except MemoryUnavailable as exc:
190
+ return json.dumps({"error": str(exc)})
191
+
192
+ @mcp.tool()
193
+ def pin_tool(id: str) -> str:
194
+ """Pin a memory entry so it is never evicted."""
195
+ try:
196
+ return pin(id)
197
+ except MemoryUnavailable as exc:
198
+ return json.dumps({"error": str(exc)})
199
+
200
+ @mcp.tool()
201
+ def link_evidence_tool(entity: str, verdict: str, source: str) -> str:
202
+ """Attach a subsystem verdict (oracle|poc|ibd|sckg|adw) to a code entity."""
203
+ try:
204
+ return link_evidence(entity, verdict, source)
205
+ except (MemoryUnavailable, ValueError) as exc:
206
+ return json.dumps({"error": str(exc)})
207
+
208
+ return list(TOOL_NAMES)
@@ -0,0 +1,313 @@
1
+ """Purpose: Pre-merge / pre-PR safety gate.
2
+
3
+ Docs: merge_safety.doc.md
4
+
5
+ Runs a battery of checks before allowing a merge or PR open:
6
+ - CoDocs coverage (broken .doc.md references)
7
+ - ceo-audit grade (cached 5 minutes per profile)
8
+ - git diff stat (large diffs flagged)
9
+ - secret scan (cheap substring heuristic)
10
+
11
+ Returns a single ``pass: bool`` plus a list of human-readable ``blockers``
12
+ and ``warnings``. The agent can use the verdict to decide whether to
13
+ proceed with the merge or fix issues first.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ import time
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Optional, Tuple
25
+
26
+ # Coarse secret pattern: keys/tokens/passwords in added lines.
27
+ _SECRET_LINE_PATTERN = re.compile(
28
+ r"(?i)(api[_-]?key|secret|token|password|passwd|pwd|access[_-]?key)\s*[:=]\s*['\"]?[A-Za-z0-9_\-\.]{16,}"
29
+ )
30
+
31
+ # Substrings that should NEVER appear in a diff (would be a leaked secret).
32
+ _SECRET_HINTS = (
33
+ "BEGIN RSA PRIVATE KEY",
34
+ "BEGIN OPENSSH PRIVATE KEY",
35
+ "BEGIN PRIVATE KEY",
36
+ "sk-", # OpenAI / many SaaS keys
37
+ "ghp_", # GitHub PAT
38
+ "github_pat_", # GitHub fine-grained PAT
39
+ "xoxb-",
40
+ "xoxp-", # Slack tokens
41
+ "AIza", # Google API keys
42
+ "AKIA", # AWS access key
43
+ "ASIA",
44
+ )
45
+
46
+ # Hard-coded fallback for the dev-machine layout.
47
+ _CEO_AUDIT_FALLBACK = "/Users/jeremy/.local/bin/sin"
48
+
49
+ # Max lines changed before we flag a "large diff" warning.
50
+ _LARGE_DIFF_LINES = 1000
51
+
52
+ # 5min — ceo-audit is the slow part; cache to keep pre-PR hooks snappy.
53
+ _CEO_AUDIT_CACHE_TTL = 300
54
+
55
+
56
+ class MergeSafety:
57
+ """Pre-merge / pre-PR safety gate.
58
+
59
+ Each check is independent and contributes to the final verdict.
60
+ All checks are best-effort — a missing CLI downgrades a check
61
+ to a warning, not a failure.
62
+ """
63
+
64
+ def __init__(self, repo_root: Optional[Path] = None) -> None:
65
+ self.repo_root = Path(repo_root) if repo_root else Path.cwd()
66
+ # Per-process cache for ceo-audit results. Key: (profile, base, head).
67
+ self._audit_cache: Dict[Tuple[str, str, str], Tuple[float, Dict[str, Any]]] = {}
68
+
69
+ def check(
70
+ self,
71
+ base: str = "main",
72
+ head: str = "HEAD",
73
+ profile: str = "QUICK",
74
+ ) -> Dict[str, Any]:
75
+ """Run all safety checks and return a verdict.
76
+
77
+ Args:
78
+ base: base ref (default ``main``).
79
+ head: head ref (default ``HEAD``).
80
+ profile: ceo-audit profile (default ``QUICK``).
81
+
82
+ Returns:
83
+ Dict with ``pass`` (bool), ``blockers`` (list of str),
84
+ ``warnings`` (list of str), ``checks`` (per-check dict),
85
+ and ``verdict`` ("READY" or "FIX_FIRST").
86
+ """
87
+ blockers: List[str] = []
88
+ warnings: List[str] = []
89
+ checks: Dict[str, Any] = {}
90
+
91
+ # ── 1. CoDocs coverage ───────────────────────────────────────
92
+ codocs = self._check_codocs()
93
+ checks["codocs"] = codocs
94
+ if codocs.get("broken", 0) > 0:
95
+ blockers.append(
96
+ f"CoDocs: {codocs['broken']} broken .doc.md reference(s) — fix before merge"
97
+ )
98
+
99
+ # ── 2. ceo-audit (cached) ────────────────────────────────────
100
+ audit = self._check_ceo_audit(profile, base, head)
101
+ checks["ceo_audit"] = audit
102
+ grade = (audit.get("grade") or "").upper()
103
+ if grade == "F":
104
+ blockers.append("ceo-audit grade F — fix critical issues before merge")
105
+ elif grade == "D":
106
+ warnings.append("ceo-audit grade D — consider improving before merge")
107
+ elif not audit.get("ok"):
108
+ # ceo-audit didn't run — downgrade to warning (don't block).
109
+ warnings.append(
110
+ f"ceo-audit could not run: {audit.get('error', 'unknown')} — verify manually"
111
+ )
112
+
113
+ # ── 3. git diff stat (size + secrets in lines) ───────────────
114
+ diff = self._check_diff(base, head)
115
+ checks["diff"] = diff
116
+ if diff.get("lines_changed", 0) > _LARGE_DIFF_LINES:
117
+ warnings.append(
118
+ f"Large diff: {diff['lines_changed']} lines changed — consider splitting"
119
+ )
120
+ secret_hits = diff.get("secret_hits", [])
121
+ if secret_hits:
122
+ blockers.append(
123
+ f"Diff contains possible secrets ({len(secret_hits)} line(s)) — "
124
+ "rotate keys and re-commit before merge"
125
+ )
126
+ checks["diff"]["secret_examples"] = secret_hits[:3]
127
+
128
+ # ── 4. Working tree must be clean ────────────────────────────
129
+ tree = self._check_tree()
130
+ checks["working_tree"] = tree
131
+ if not tree.get("clean", False):
132
+ warnings.append(
133
+ f"Working tree is dirty ({tree.get('changes_count', '?')} change(s)) — "
134
+ "commit/stash before merge"
135
+ )
136
+
137
+ # ── Compose verdict ──────────────────────────────────────────
138
+ passed = len(blockers) == 0
139
+ verdict = "READY" if passed else "FIX_FIRST"
140
+
141
+ return {
142
+ "pass": passed,
143
+ "verdict": verdict,
144
+ "blockers": blockers,
145
+ "warnings": warnings,
146
+ "checks": checks,
147
+ "base": base,
148
+ "head": head,
149
+ "profile": profile,
150
+ "timestamp": _now_iso(),
151
+ }
152
+
153
+ # ── helpers ─────────────────────────────────────────────────────
154
+ def _check_codocs(self) -> Dict[str, Any]:
155
+ try:
156
+ from . import codocs
157
+
158
+ broken = codocs.find_broken(str(self.repo_root))
159
+ return {
160
+ "ok": not bool(broken),
161
+ "broken": len(broken),
162
+ "items": [b.to_dict() for b in broken][:10],
163
+ }
164
+ except Exception as exc:
165
+ return {"ok": True, "broken": 0, "error": str(exc)}
166
+
167
+ def _check_ceo_audit(
168
+ self,
169
+ profile: str,
170
+ base: str,
171
+ head: str,
172
+ ) -> Dict[str, Any]:
173
+ """Run ceo-audit, with a 5-minute in-process cache."""
174
+ cache_key = (profile, base, head)
175
+ now = time.time()
176
+
177
+ if cache_key in self._audit_cache:
178
+ ts, data = self._audit_cache[cache_key]
179
+ if (now - ts) < _CEO_AUDIT_CACHE_TTL:
180
+ return {**data, "cache_hit": True}
181
+ # Stale — fall through and re-run.
182
+
183
+ try:
184
+ sin_bin = shutil.which("sin") or _CEO_AUDIT_FALLBACK
185
+ if not Path(sin_bin).exists():
186
+ return {"ok": False, "error": "sin CLI not installed"}
187
+
188
+ proc = subprocess.run(
189
+ [
190
+ sin_bin,
191
+ "ceo-audit",
192
+ "run",
193
+ str(self.repo_root),
194
+ f"--profile={profile}",
195
+ "--json",
196
+ ],
197
+ capture_output=True,
198
+ text=True,
199
+ timeout=180, # 3min ceiling
200
+ )
201
+ if proc.returncode == 0 and proc.stdout.strip():
202
+ data = json.loads(proc.stdout)
203
+ result = {
204
+ "ok": True,
205
+ "grade": data.get("grade"),
206
+ "report_path": data.get("report_path"),
207
+ }
208
+ self._audit_cache[cache_key] = (now, result)
209
+ return result
210
+ return {"ok": False, "error": proc.stderr[-300:]}
211
+ except (subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
212
+ return {"ok": False, "error": str(exc)}
213
+ except Exception as exc:
214
+ return {"ok": False, "error": str(exc)}
215
+
216
+ def _check_diff(self, base: str, head: str) -> Dict[str, Any]:
217
+ try:
218
+ proc = subprocess.run(
219
+ ["git", "diff", "--shortstat", f"{base}...{head}"],
220
+ cwd=self.repo_root,
221
+ capture_output=True,
222
+ text=True,
223
+ timeout=15,
224
+ )
225
+ shortstat = proc.stdout.strip() if proc.returncode == 0 else ""
226
+ # " 3 files changed, 42 insertions(+), 17 deletions(-)"
227
+ lines_changed = _parse_shortstat(shortstat)
228
+
229
+ # Fetch the actual diff content for secret scan.
230
+ content_proc = subprocess.run(
231
+ ["git", "diff", f"{base}...{head}"],
232
+ cwd=self.repo_root,
233
+ capture_output=True,
234
+ text=True,
235
+ timeout=30,
236
+ )
237
+ content = content_proc.stdout if content_proc.returncode == 0 else ""
238
+ secret_hits = _scan_for_secrets(content)
239
+
240
+ return {
241
+ "ok": True,
242
+ "lines_changed": lines_changed,
243
+ "shortstat": shortstat,
244
+ "secret_hits": secret_hits,
245
+ }
246
+ except subprocess.TimeoutExpired:
247
+ return {"ok": False, "lines_changed": 0, "secret_hits": [], "error": "git diff timeout"}
248
+ except Exception as exc:
249
+ return {"ok": False, "lines_changed": 0, "secret_hits": [], "error": str(exc)}
250
+
251
+ def _check_tree(self) -> Dict[str, Any]:
252
+ try:
253
+ proc = subprocess.run(
254
+ ["git", "status", "--porcelain"],
255
+ cwd=self.repo_root,
256
+ capture_output=True,
257
+ text=True,
258
+ timeout=5,
259
+ )
260
+ if proc.returncode == 0:
261
+ changes = proc.stdout.strip().splitlines()
262
+ return {"ok": True, "clean": not bool(changes), "changes_count": len(changes)}
263
+ return {"ok": False, "clean": False, "error": proc.stderr[-200:]}
264
+ except Exception as exc:
265
+ return {"ok": False, "clean": False, "error": str(exc)}
266
+
267
+
268
+ # ── module-level helpers ────────────────────────────────────────────
269
+
270
+
271
+ def _parse_shortstat(shortstat: str) -> int:
272
+ """Parse ``3 files changed, 42 insertions(+), 17 deletions(-)`` → 59."""
273
+ m = re.search(r"(\d+)\s+insertion", shortstat)
274
+ insertions = int(m.group(1)) if m else 0
275
+ m = re.search(r"(\d+)\s+deletion", shortstat)
276
+ deletions = int(m.group(1)) if m else 0
277
+ return insertions + deletions
278
+
279
+
280
+ def _scan_for_secrets(diff_content: str) -> List[str]:
281
+ """Return a list of human-readable hits for any secret-like content.
282
+
283
+ Two passes:
284
+ 1. Substring scan for known SaaS key prefixes.
285
+ 2. Regex for ``key = "value"`` / ``token: "value"`` patterns.
286
+ """
287
+ hits: List[str] = []
288
+
289
+ # Pass 1: substring hints
290
+ for hint in _SECRET_HINTS:
291
+ if hint in diff_content:
292
+ # Find a representative line for the report.
293
+ for i, line in enumerate(diff_content.splitlines(), 1):
294
+ if hint in line:
295
+ hits.append(f"line {i}: contains '{hint}'")
296
+ break
297
+
298
+ # Pass 2: regex (only added lines, not context — context can be noisy).
299
+ for i, line in enumerate(diff_content.splitlines(), 1):
300
+ if not line.startswith("+"):
301
+ continue
302
+ if _SECRET_LINE_PATTERN.search(line):
303
+ hits.append(f"line {i}: key=value pattern")
304
+ if len(hits) >= 20:
305
+ break
306
+
307
+ return hits
308
+
309
+
310
+ def _now_iso() -> str:
311
+ from datetime import datetime, timezone
312
+
313
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,102 @@
1
+ """Purpose: Isolated worktree orchestration — parallel agent tasks without conflicts.
2
+
3
+ Docs: orchestration_worktrees.doc.md
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import Callable, Optional
14
+
15
+
16
+ class SINWorktreeOrchestrator:
17
+ """Manages isolated git worktrees for parallel agent task execution."""
18
+
19
+ def __init__(self, repo_root: Optional[Path] = None):
20
+ self.repo_root = repo_root or Path.cwd()
21
+ self.active_worktrees: list[Path] = []
22
+
23
+ def is_git_repo(self) -> bool:
24
+ return (self.repo_root / ".git").exists()
25
+
26
+ def create_worktree(self, branch_name: Optional[str] = None) -> dict:
27
+ if not self.is_git_repo():
28
+ return {"error": "Not a git repository. Worktree isolation requires git."}
29
+ # Sibling-dir layout (.sin-worktrees-<name>) keeps the worktrees
30
+ # OUTSIDE the repo's own working tree. That matters because git
31
+ # refuses to checkout branches into a worktree that overlaps the
32
+ # main repo's `.git`-tracked files (silent failure mode).
33
+ branch = (
34
+ branch_name or f"sin-task-{uuid.uuid4().hex[:8]}"
35
+ ) # 8 hex chars = 32 bits, plenty unique for worktrees
36
+ worktree_path = self.repo_root.parent / f".sin-worktrees-{self.repo_root.name}" / branch
37
+ try:
38
+ subprocess.run(
39
+ ["git", "worktree", "add", str(worktree_path), "-b", branch],
40
+ cwd=self.repo_root,
41
+ check=True,
42
+ capture_output=True,
43
+ text=True,
44
+ )
45
+ self.active_worktrees.append(worktree_path)
46
+ return {
47
+ "success": True,
48
+ "worktree_path": str(worktree_path),
49
+ "branch": branch,
50
+ "message": f"Isolated worktree created at {worktree_path}",
51
+ }
52
+ except subprocess.CalledProcessError as e:
53
+ return {"error": f"Git worktree creation failed: {e.stderr}"}
54
+
55
+ def execute_in_worktree(self, worktree_path: str, task_func: Callable, *args, **kwargs) -> dict:
56
+ original_cwd = os.getcwd()
57
+ try:
58
+ os.chdir(worktree_path)
59
+ result = task_func(*args, **kwargs)
60
+ return {"success": True, "result": result}
61
+ except Exception as e:
62
+ return {"success": False, "error": str(e)}
63
+ finally:
64
+ os.chdir(original_cwd)
65
+
66
+ def cleanup_worktree(self, worktree_path: str, merge_back: bool = False) -> dict:
67
+ path = Path(worktree_path)
68
+ if path not in self.active_worktrees:
69
+ return {"error": "Worktree not managed by this orchestrator"}
70
+ try:
71
+ if merge_back:
72
+ branch = path.name
73
+ subprocess.run(
74
+ ["git", "checkout", "main"], cwd=self.repo_root, check=True, capture_output=True
75
+ )
76
+ merge_result = subprocess.run(
77
+ [
78
+ "git",
79
+ "merge",
80
+ "--no-ff",
81
+ branch,
82
+ "-m",
83
+ f"Auto-merge from SIN worktree: {branch}",
84
+ ],
85
+ cwd=self.repo_root,
86
+ capture_output=True,
87
+ text=True,
88
+ )
89
+ if merge_result.returncode != 0:
90
+ return {"error": f"Merge conflict: {merge_result.stderr}"}
91
+ subprocess.run(
92
+ ["git", "worktree", "remove", str(path), "--force"],
93
+ cwd=self.repo_root,
94
+ check=True,
95
+ capture_output=True,
96
+ )
97
+ self.active_worktrees.remove(path)
98
+ if path.exists():
99
+ shutil.rmtree(path)
100
+ return {"success": True, "message": "Worktree cleaned up successfully"}
101
+ except subprocess.CalledProcessError as e:
102
+ return {"error": f"Worktree cleanup failed: {e.stderr}"}