loom-learn 0.3.0__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 (47) hide show
  1. loom/__init__.py +15 -0
  2. loom/cli/__init__.py +1 -0
  3. loom/cli/main.py +569 -0
  4. loom/coaching/__init__.py +5 -0
  5. loom/coaching/amplifier.py +478 -0
  6. loom/config.py +23 -0
  7. loom/engine/__init__.py +23 -0
  8. loom/engine/auto_observer.py +785 -0
  9. loom/engine/context_loader.py +602 -0
  10. loom/engine/decay_manager.py +75 -0
  11. loom/engine/domain_extractor.py +159 -0
  12. loom/engine/llm_extractor.py +103 -0
  13. loom/engine/org_store.py +812 -0
  14. loom/engine/retention.py +460 -0
  15. loom/engine/rule_store.py +251 -0
  16. loom/engine/timeline.py +432 -0
  17. loom/llm/__init__.py +15 -0
  18. loom/llm/anthropic.py +110 -0
  19. loom/llm/base.py +101 -0
  20. loom/llm/deepseek.py +108 -0
  21. loom/llm/factory.py +60 -0
  22. loom/llm/gemini.py +82 -0
  23. loom/mcp/__init__.py +24 -0
  24. loom/mcp/__main__.py +16 -0
  25. loom/mcp/proxy.py +338 -0
  26. loom/mcp/server.py +2357 -0
  27. loom/onboarding/__init__.py +9 -0
  28. loom/onboarding/packs.py +312 -0
  29. loom/onboarding/succession.py +498 -0
  30. loom/security/__init__.py +20 -0
  31. loom/security/access.py +74 -0
  32. loom/security/audit.py +97 -0
  33. loom/security/integrity.py +61 -0
  34. loom/security/private_mode.py +51 -0
  35. loom/security/rbac.py +445 -0
  36. loom/security/redactor.py +43 -0
  37. loom/security/tests/test_security.py +197 -0
  38. loom/storage/__init__.py +6 -0
  39. loom/storage/adapters.py +552 -0
  40. loom/storage/backend.py +78 -0
  41. loom/storage/migrations/001_initial.sql +129 -0
  42. loom/storage/postgres_store.py +761 -0
  43. loom_learn-0.3.0.dist-info/METADATA +17 -0
  44. loom_learn-0.3.0.dist-info/RECORD +47 -0
  45. loom_learn-0.3.0.dist-info/WHEEL +5 -0
  46. loom_learn-0.3.0.dist-info/entry_points.txt +2 -0
  47. loom_learn-0.3.0.dist-info/top_level.txt +1 -0
loom/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Loom — The memory layer for AI agents.
2
+
3
+ Glen-level features:
4
+ - Auto-capture (passive observation)
5
+ - Auto-recall (pre-loaded context every session)
6
+ - Org-wide shared memory (one repository for the whole org)
7
+ - Per-observation RBAC (agents see only what their user is cleared to see)
8
+ - Tiered retention (permanent org knowledge + decaying conventions)
9
+ - Auditable timeline (one queryable history of the organization)
10
+ - Instant onboarding (new hire's agent already knows the org)
11
+ - Succession capture (knowledge survives turnover)
12
+ - Coaching amplification (top performer patterns scale across the team)
13
+ """
14
+
15
+ __version__ = "0.3.0"
loom/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Loom CLI — setup, doctor, and admin commands."""
loom/cli/main.py ADDED
@@ -0,0 +1,569 @@
1
+ """Loom CLI — one-shot setup and health checks."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+
10
+ def _python_path() -> str:
11
+ """Return the full path to the current Python interpreter."""
12
+ return sys.executable
13
+
14
+
15
+ def _is_windows() -> bool:
16
+ return sys.platform == "win32"
17
+
18
+
19
+ def cmd_setup(args=None):
20
+ """Generate a ready-to-paste MCP config for Claude Desktop."""
21
+ project_root = os.environ.get(
22
+ "LOOM_PROJECT_ROOT",
23
+ str(Path.home() / "loom-memory"),
24
+ )
25
+
26
+ # Auto-create the directory
27
+ Path(project_root).mkdir(parents=True, exist_ok=True)
28
+
29
+ python = _python_path()
30
+ is_mac = sys.platform == "darwin"
31
+ is_win = _is_windows()
32
+
33
+ if is_mac:
34
+ config_path = "~/Library/Application Support/Claude/claude_desktop_config.json"
35
+ elif is_win:
36
+ config_path = "%APPDATA%\\Claude\\claude_desktop_config.json"
37
+ else:
38
+ config_path = "~/.config/Claude/claude_desktop_config.json"
39
+
40
+ print("=" * 60)
41
+ print(" Loom MCP Server — One-Shot Setup")
42
+ print("=" * 60)
43
+ print()
44
+ print(f" Project root : {project_root}")
45
+ print(f" Python : {python}")
46
+ print(f" Config file : {config_path}")
47
+ print()
48
+
49
+ # Base config (no API key)
50
+ base_config = {
51
+ "mcpServers": {
52
+ "loom": {
53
+ "command": python,
54
+ "args": ["-m", "loom.mcp"],
55
+ "env": {
56
+ "LOOM_PROJECT_ROOT": project_root,
57
+ },
58
+ }
59
+ }
60
+ }
61
+
62
+ print("── Paste this into your Claude config file: ──")
63
+ print()
64
+ print(json.dumps(base_config, indent=2))
65
+ print()
66
+
67
+ # API key options
68
+ has_anthropic = bool(os.environ.get("ANTHROPIC_API_KEY"))
69
+ has_deepseek = bool(os.environ.get("LOOM_DEEPSEEK_API_KEY"))
70
+ has_gemini = bool(os.environ.get("GEMINI_API_KEY"))
71
+
72
+ if has_anthropic or has_deepseek or has_gemini:
73
+ print("── Detected API keys in your environment ──")
74
+ print()
75
+ for name, key, env_var in [
76
+ ("Anthropic", has_anthropic, "ANTHROPIC_API_KEY"),
77
+ ("DeepSeek", has_deepseek, "LOOM_DEEPSEEK_API_KEY"),
78
+ ("Gemini", has_gemini, "GEMINI_API_KEY"),
79
+ ]:
80
+ if key:
81
+ masked = os.environ[env_var][:7] + "..." if os.environ[env_var] else ""
82
+ print(f" {name}: {masked} (from ${env_var})")
83
+ print()
84
+ print(" These keys were auto-detected and will be used if you")
85
+ print(" paste the config above. No extra config needed.")
86
+ else:
87
+ print("── Optional: Add an LLM for smarter extraction ──")
88
+ print()
89
+ print(" Copy one of these into the \"env\" block above:")
90
+ print()
91
+ print(' "ANTHROPIC_API_KEY": "sk-ant-..."')
92
+ print(' or')
93
+ print(' "LOOM_LLM_PROVIDER": "deepseek",')
94
+ print(' "LOOM_DEEPSEEK_API_KEY": "sk-..."')
95
+ print(' or')
96
+ print(' "LOOM_LLM_PROVIDER": "gemini",')
97
+ print(' "GEMINI_API_KEY": "..."')
98
+ print()
99
+ print(" Without a key, Loom uses free keyword extraction.")
100
+
101
+ print()
102
+ print("── Next steps ──")
103
+ print()
104
+ print(f" 1. Paste the JSON above into {config_path}")
105
+ print(" 2. Restart Claude Desktop")
106
+ print(" 3. Run: loom doctor")
107
+ print()
108
+
109
+
110
+ def cmd_doctor(args=None):
111
+ """Check everything is working."""
112
+ print("=" * 60)
113
+ print(" Loom Doctor — System Check")
114
+ print("=" * 60)
115
+ print()
116
+
117
+ checks = []
118
+
119
+ # 1. Python version
120
+ py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
121
+ py_ok = sys.version_info >= (3, 11)
122
+ checks.append(("Python 3.11+", py_ok, f"Python {py_version}"))
123
+
124
+ # 2. Loom importable
125
+ try:
126
+ import loom
127
+ loom_ok = True
128
+ loom_msg = f"Loom v{loom.__version__}"
129
+ except ImportError:
130
+ loom_ok = False
131
+ loom_msg = "Loom not installed — run: pip install -e ."
132
+ checks.append(("Loom installed", loom_ok, loom_msg))
133
+
134
+ # 3. Project root exists and is writable
135
+ project_root = Path(os.environ.get("LOOM_PROJECT_ROOT", os.getcwd()))
136
+ root_exists = project_root.exists()
137
+ root_writable = os.access(project_root, os.W_OK) if root_exists else False
138
+ checks.append(("Project directory", root_exists, str(project_root)))
139
+ checks.append(("Directory writable", root_writable,
140
+ "writable" if root_writable else f"not writable: {project_root}"))
141
+
142
+ # 4. .loom/ directory
143
+ loom_dir = project_root / ".loom"
144
+ loom_dir_ok = loom_dir.exists()
145
+ checks.append((".loom/ exists", loom_dir_ok,
146
+ str(loom_dir) if loom_dir_ok else "will be created on first use"))
147
+
148
+ # 5. Rules and timeline
149
+ if loom_dir_ok:
150
+ rules_file = loom_dir / "rules.json"
151
+ if rules_file.exists():
152
+ try:
153
+ data = json.loads(rules_file.read_text())
154
+ rule_count = len(data.get("rules", []))
155
+ checks.append(("Rules stored", True, f"{rule_count} rules"))
156
+ except Exception:
157
+ checks.append(("Rules stored", False, "rules.json corrupted"))
158
+ else:
159
+ checks.append(("Rules stored", True, "no rules yet (fresh install)"))
160
+
161
+ timeline = loom_dir / "timeline.jsonl"
162
+ if timeline.exists():
163
+ entries = timeline.read_text().strip().splitlines()
164
+ checks.append(("Timeline", True, f"{len(entries)} entries"))
165
+ else:
166
+ checks.append(("Timeline", True, "no entries yet"))
167
+
168
+ # 6. Domain configs
169
+ domains_dir = loom_dir / "domains" if loom_dir_ok else None
170
+ if domains_dir and domains_dir.exists():
171
+ configs = list(domains_dir.glob("*.yml"))
172
+ checks.append(("Domain configs", True, f"{len(configs)} domains"))
173
+ else:
174
+ checks.append(("Domain configs", True, "created on first use"))
175
+
176
+ # 7. LLM Provider
177
+ from loom.llm.factory import get_provider
178
+ provider = get_provider()
179
+ if provider:
180
+ sdk_ok = False
181
+ if provider.provider_name == "anthropic":
182
+ try:
183
+ import anthropic
184
+ sdk_ok = True
185
+ except ImportError:
186
+ pass
187
+ elif provider.provider_name == "deepseek":
188
+ try:
189
+ import openai
190
+ sdk_ok = True
191
+ except ImportError:
192
+ pass
193
+ elif provider.provider_name == "gemini":
194
+ try:
195
+ import google.generativeai
196
+ sdk_ok = True
197
+ except ImportError:
198
+ pass
199
+
200
+ sdk_msg = f"{provider.provider_name} (SDK: {'installed' if sdk_ok else 'MISSING — pip install loom-agent[{provider.provider_name}]'})"
201
+ checks.append(("LLM extraction", sdk_ok, sdk_msg))
202
+ else:
203
+ checks.append(("LLM extraction", True, "keyword only (free, no API key)"))
204
+
205
+ # 8. MCP protocol check
206
+ try:
207
+ from mcp.server.fastmcp import FastMCP
208
+ mcp_ok = True
209
+ mcp_msg = "FastMCP available"
210
+ except ImportError:
211
+ mcp_ok = False
212
+ mcp_msg = "mcp package not installed"
213
+ checks.append(("MCP protocol", mcp_ok, mcp_msg))
214
+
215
+ # Print results
216
+ all_ok = True
217
+ for name, ok, detail in checks:
218
+ status = "PASS" if ok else "FAIL"
219
+ if not ok:
220
+ all_ok = False
221
+ print(f" [{status}] {name}")
222
+ if detail:
223
+ print(f" {detail}")
224
+
225
+ print()
226
+ if all_ok:
227
+ print(" All checks passed. Loom is ready.")
228
+ else:
229
+ print(" Some checks failed. Fix the FAIL items above.")
230
+ print()
231
+ print(" Quick fixes:")
232
+ print(" pip install -e . # install Loom")
233
+ print(" pip install openai # for DeepSeek")
234
+ print(" pip install anthropic # for Anthropic")
235
+ print(" pip install google-generativeai # for Gemini")
236
+ print(" export LOOM_PROJECT_ROOT=/path/to/your/project")
237
+
238
+ print()
239
+ return 0 if all_ok else 1
240
+
241
+
242
+ def _claude_config_path() -> Path | None:
243
+ """Return the OS-specific Claude Desktop config path, or None."""
244
+ home = Path.home()
245
+ if sys.platform == "darwin":
246
+ return home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
247
+ elif sys.platform == "win32":
248
+ return Path(os.environ.get("APPDATA", "")) / "Claude" / "claude_desktop_config.json"
249
+ else:
250
+ return home / ".config" / "Claude" / "claude_desktop_config.json"
251
+
252
+
253
+ def cmd_preflight(config_path: str | None = None):
254
+ """Validate that Loom will work when Claude Desktop launches it.
255
+
256
+ Parses the Claude Desktop config, checks the Python path, verifies
257
+ Loom is importable, and validates the MCP transport chain.
258
+ """
259
+ import subprocess
260
+
261
+ print("=" * 60)
262
+ print(" Loom Preflight — MCP Chain Validation")
263
+ print("=" * 60)
264
+ print()
265
+
266
+ # Resolve config path
267
+ if config_path:
268
+ cfg = Path(config_path).expanduser()
269
+ else:
270
+ cfg = _claude_config_path()
271
+
272
+ print(f" Config: {cfg}")
273
+ print()
274
+
275
+ checks = []
276
+ all_ok = True
277
+
278
+ # 1. Config file exists and is valid JSON
279
+ if not cfg.exists():
280
+ print(f" [FAIL] Config file not found: {cfg}")
281
+ print(f" Run 'loom setup' first, or use --config-path to specify")
282
+ print()
283
+ return 1
284
+ else:
285
+ try:
286
+ config_data = json.loads(cfg.read_text())
287
+ checks.append(("Config file", True, "found and valid JSON"))
288
+ except json.JSONDecodeError as e:
289
+ print(f" [FAIL] Config file is not valid JSON: {e}")
290
+ print()
291
+ return 1
292
+
293
+ # 2. Find Loom in the mcpServers block
294
+ mcp_servers = config_data.get("mcpServers", {})
295
+ loom_config = mcp_servers.get("loom")
296
+ if not loom_config:
297
+ print(f" [FAIL] No 'loom' entry found in mcpServers")
298
+ print(f" Run 'loom setup' and paste its output into the config.")
299
+ print()
300
+ return 1
301
+
302
+ command = loom_config.get("command", "")
303
+ args_list = loom_config.get("args", [])
304
+ env_vars = loom_config.get("env", {})
305
+
306
+ # 3. Python executable exists and is executable
307
+ python_exe = command
308
+ if not python_exe:
309
+ python_exe = shutil.which("python3") or shutil.which("python") or ""
310
+ if not python_exe:
311
+ print(f" [FAIL] No Python command found in config")
312
+ print(f" Set 'command' to your Python path (e.g., which python3)")
313
+ print()
314
+ return 1
315
+
316
+ exe_path = Path(python_exe)
317
+ if not exe_path.is_file() and not shutil.which(python_exe):
318
+ print(f" [FAIL] Python not found: {python_exe}")
319
+ print(f" Full path or install Python 3.10+ and retry.")
320
+ print()
321
+ return 1
322
+ checks.append(("Python path", True, str(python_exe)))
323
+
324
+ # 4. Loom is importable
325
+ try:
326
+ result = subprocess.run(
327
+ [python_exe, "-c", "import loom"],
328
+ capture_output=True, text=True, timeout=10,
329
+ )
330
+ if result.returncode == 0:
331
+ checks.append(("Loom import", True, "loom package found"))
332
+ else:
333
+ print(f" [FAIL] Loom package not importable from {python_exe}")
334
+ print(f" {result.stderr.strip()}")
335
+ print(f" Run: {python_exe} -m pip install loom-agent")
336
+ print()
337
+ return 1
338
+ except subprocess.TimeoutExpired:
339
+ print(f" [FAIL] Python import check timed out after 10s")
340
+ print()
341
+ return 1
342
+ except Exception as e:
343
+ print(f" [FAIL] Cannot run Python: {e}")
344
+ print()
345
+ return 1
346
+
347
+ # 5. Storage path is writable (if configured)
348
+ project_root = env_vars.get("LOOM_PROJECT_ROOT", "")
349
+ if project_root:
350
+ pr = Path(project_root).expanduser()
351
+ if pr.exists():
352
+ if os.access(pr, os.W_OK):
353
+ checks.append(("Storage path", True, f"{pr} (writable)"))
354
+ else:
355
+ checks.append(("Storage path", False, f"{pr} (NOT writable — check permissions)"))
356
+ all_ok = False
357
+ else:
358
+ try:
359
+ pr.mkdir(parents=True, exist_ok=True)
360
+ checks.append(("Storage path", True, f"{pr} (created)"))
361
+ except Exception as e:
362
+ checks.append(("Storage path", False, f"{pr} (cannot create: {e})"))
363
+ all_ok = False
364
+ else:
365
+ checks.append(("Storage path", True, "not set (defaults to $PWD at runtime)"))
366
+
367
+ # 6. MCP module loads
368
+ try:
369
+ result = subprocess.run(
370
+ [python_exe, "-c", "from loom.mcp.server import LoomMCPServer"],
371
+ capture_output=True, text=True, timeout=10,
372
+ )
373
+ if result.returncode == 0:
374
+ checks.append(("MCP module", True, "loom.mcp.server loads"))
375
+ else:
376
+ print(f" [FAIL] loom.mcp.server failed to load")
377
+ print(f" {result.stderr.strip()}")
378
+ print()
379
+ return 1
380
+ except subprocess.TimeoutExpired:
381
+ print(f" [FAIL] MCP module load check timed out after 10s")
382
+ print()
383
+ return 1
384
+
385
+ # Print results
386
+ print()
387
+ for name, ok, detail in checks:
388
+ status = "PASS" if ok else "FAIL"
389
+ if not ok:
390
+ all_ok = False
391
+ print(f" [{status}] {name}")
392
+ if detail:
393
+ print(f" {detail}")
394
+
395
+ print()
396
+ if all_ok:
397
+ print(" All preflight checks passed. Ready to restart Claude Desktop.")
398
+ else:
399
+ print(" Some checks failed. Fix the FAIL items above.")
400
+ print()
401
+ return 0 if all_ok else 1
402
+
403
+
404
+ def cmd_cloud_setup(args=None):
405
+ """Create a Supabase-backed shared Loom database and print config."""
406
+ import urllib.request
407
+ import urllib.error
408
+
409
+ print("=" * 60)
410
+ print(" Loom Cloud Setup — Shared Team Memory")
411
+ print("=" * 60)
412
+ print()
413
+ print(" This creates a shared database so your entire team")
414
+ print(" shares the same conventions in real-time.")
415
+ print()
416
+
417
+ # Get Supabase credentials
418
+ supabase_url = input(" Supabase URL (e.g. https://xyz.supabase.co): ").strip()
419
+ if not supabase_url:
420
+ print(" URL is required.")
421
+ return
422
+
423
+ supabase_key = input(" Supabase service_role key (sbp_...): ").strip()
424
+ if not supabase_key:
425
+ print(" Key is required.")
426
+ return
427
+
428
+ project_name = input(" Project name [default: loom-shared]: ").strip()
429
+ if not project_name:
430
+ project_name = "loom-shared"
431
+
432
+ print()
433
+ print(" Creating database...")
434
+
435
+ # Build the Postgres connection URL from Supabase params
436
+ # Supabase URL: https://[ref].supabase.co
437
+ # DB URL: postgresql://postgres:[key]@db.[ref].supabase.co:5432/postgres
438
+ try:
439
+ ref = supabase_url.replace("https://", "").replace(".supabase.co", "").strip("/")
440
+ db_url = f"postgresql://postgres:{supabase_key}@db.{ref}.supabase.co:5432/postgres"
441
+ except Exception:
442
+ print(" Invalid Supabase URL format. Expected: https://[ref].supabase.co")
443
+ return
444
+
445
+ # Run migrations
446
+ try:
447
+ from loom.storage.postgres_store import PostgresStore
448
+ from loom.config import StorageConfig
449
+
450
+ config = StorageConfig(
451
+ backend="postgres",
452
+ database_url=db_url,
453
+ )
454
+ store = PostgresStore(config)
455
+ store.initialize()
456
+
457
+ if store.health_check():
458
+ print(" Database: connected")
459
+ else:
460
+ print(" Database: connection failed — check your URL and key")
461
+ return
462
+ except ImportError:
463
+ print(" psycopg2 not installed. Run: pip install loom-agent[cloud]")
464
+ return
465
+ except Exception as e:
466
+ print(f" Error: {e}")
467
+ return
468
+
469
+ # Generate API key
470
+ import secrets
471
+ import hashlib
472
+ api_key = "loom_sk_" + secrets.token_hex(24)
473
+ key_hash = hashlib.sha256(api_key.encode()).hexdigest()
474
+
475
+ try:
476
+ with store._conn() as conn:
477
+ with conn.cursor() as cur:
478
+ cur.execute(
479
+ "INSERT INTO api_keys (key_hash, key_prefix, project_id, role) "
480
+ "VALUES (%s, %s, %s, %s)",
481
+ (key_hash, api_key[:10] + "...", project_name, "admin"),
482
+ )
483
+ conn.commit()
484
+ except Exception:
485
+ pass # key storage is best-effort
486
+
487
+ # Generate config
488
+ python_path = sys.executable
489
+ config = {
490
+ "mcpServers": {
491
+ "loom": {
492
+ "command": python_path,
493
+ "args": ["-m", "loom.mcp"],
494
+ "env": {
495
+ "LOOM_STORAGE_BACKEND": "postgres",
496
+ "LOOM_DATABASE_URL": db_url,
497
+ },
498
+ }
499
+ }
500
+ }
501
+
502
+ print()
503
+ print("=" * 60)
504
+ print(" Paste this into your Claude Desktop config:")
505
+ print("=" * 60)
506
+ print()
507
+ print(json.dumps(config, indent=2))
508
+ print()
509
+ print(" Share this config with your team.")
510
+ print(" Everyone connects to the same memory.")
511
+ print()
512
+ print(" ⚠️ SECURITY: This config contains database credentials.")
513
+ print(" Restrict file permissions: chmod 600 ~/Library/Application\\\\")
514
+ print(" Support/Claude/claude_desktop_config.json")
515
+ print(" Do not commit this file to git — it contains your Supabase")
516
+ print(" service_role key which has full database access.")
517
+ print()
518
+ print(" API key (for SaaS later): " + api_key)
519
+ print()
520
+
521
+
522
+ def main():
523
+ if len(sys.argv) < 2:
524
+ print("Usage: loom <command>")
525
+ print()
526
+ print("Commands:")
527
+ print(" setup Generate local Claude Desktop config")
528
+ print(" init Same as setup — initialize Loom in this project")
529
+ print(" cloud setup Create a shared Supabase database for your team")
530
+ print(" doctor Check everything is working")
531
+ print(" doctor --preflight Validate MCP config before restart")
532
+ print()
533
+ print("Quick start (local):")
534
+ print(" 1. loom setup — paste into Claude config")
535
+ print(" 2. restart Claude Desktop")
536
+ print(" 3. loom doctor — verify everything is green")
537
+ print()
538
+ print("Quick start (team):")
539
+ print(" 1. loom cloud setup — paste Supabase URL + key")
540
+ print(" 2. share the config with your team")
541
+ sys.exit(0)
542
+
543
+ cmd = sys.argv[1]
544
+ if cmd == "setup":
545
+ cmd_setup()
546
+ elif cmd == "cloud" and len(sys.argv) > 2 and sys.argv[2] == "setup":
547
+ cmd_cloud_setup()
548
+ elif cmd == "doctor":
549
+ if "--preflight" in sys.argv:
550
+ # Extract optional --config-path argument
551
+ cp_idx = None
552
+ try:
553
+ cp_idx = sys.argv.index("--config-path")
554
+ except ValueError:
555
+ pass
556
+ config_path = sys.argv[cp_idx + 1] if cp_idx and cp_idx + 1 < len(sys.argv) else None
557
+ sys.exit(cmd_preflight(config_path))
558
+ else:
559
+ sys.exit(cmd_doctor())
560
+ elif cmd == "init":
561
+ cmd_setup()
562
+ else:
563
+ print(f"Unknown command: {cmd}")
564
+ print("Run 'loom' without arguments to see available commands.")
565
+ sys.exit(1)
566
+
567
+
568
+ if __name__ == "__main__":
569
+ main()
@@ -0,0 +1,5 @@
1
+ """Loom coaching — amplify top performers' patterns across the entire team."""
2
+
3
+ from .amplifier import CoachingAmplifier, AmplifiedRule
4
+
5
+ __all__ = ["CoachingAmplifier", "AmplifiedRule"]