synap-git 2.1.6__tar.gz → 2.1.7__tar.gz

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 (57) hide show
  1. {synap_git-2.1.6 → synap_git-2.1.7}/.gitignore +4 -0
  2. {synap_git-2.1.6 → synap_git-2.1.7}/CHANGELOG.md +8 -1
  3. {synap_git-2.1.6 → synap_git-2.1.7}/PKG-INFO +4 -6
  4. {synap_git-2.1.6 → synap_git-2.1.7}/README.md +3 -5
  5. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/__init__.py +1 -1
  6. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/api/app.py +1 -1
  7. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/main.py +96 -77
  8. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/config.py +0 -6
  9. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/diagnostics/logger.py +4 -0
  10. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/git/state.py +1 -1
  11. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/daemon.py +7 -64
  12. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/engine.py +88 -59
  13. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/scanner.py +2 -2
  14. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/wiki.py +4 -4
  15. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/mcp/server.py +2 -2
  16. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/parser/registry.py +1 -1
  17. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/factory.py +4 -3
  18. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/retrieval/engine.py +8 -6
  19. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/storage/sqlite.py +24 -1
  20. synap_git-2.1.7/website/README.md +36 -0
  21. synap_git-2.1.6/src/synap_git/cli/mcp.py +0 -137
  22. {synap_git-2.1.6 → synap_git-2.1.7}/.synap.example/README.md +0 -0
  23. {synap_git-2.1.6 → synap_git-2.1.7}/LICENSE.md +0 -0
  24. {synap_git-2.1.6 → synap_git-2.1.7}/pyproject.toml +0 -0
  25. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/api/__init__.py +0 -0
  26. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/api/static/index.html +0 -0
  27. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/__init__.py +0 -0
  28. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/__main__.py +0 -0
  29. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/checkpoint.py +0 -0
  30. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/lessons.py +0 -0
  31. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/memory.py +0 -0
  32. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/usage.py +0 -0
  33. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/wiki.py +0 -0
  34. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/diagnostics/__init__.py +0 -0
  35. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/diagnostics/tracing.py +0 -0
  36. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/embeddings/__init__.py +0 -0
  37. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/git/__init__.py +0 -0
  38. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/__init__.py +0 -0
  39. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/mcp/__init__.py +0 -0
  40. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/parser/__init__.py +0 -0
  41. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/__init__.py +0 -0
  42. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/anthropic.py +0 -0
  43. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/base.py +0 -0
  44. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/gemini.py +0 -0
  45. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/ollama.py +0 -0
  46. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/openai.py +0 -0
  47. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/openrouter.py +0 -0
  48. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/py.typed +0 -0
  49. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/retrieval/__init__.py +0 -0
  50. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/retrieval/memory.py +0 -0
  51. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/__init__.py +0 -0
  52. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/redactor.py +0 -0
  53. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/sanitizer.py +0 -0
  54. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/validator.py +0 -0
  55. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/storage/__init__.py +0 -0
  56. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/utils/__init__.py +0 -0
  57. {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/utils/serialization.py +0 -0
@@ -25,3 +25,7 @@ build/
25
25
  .synapse/
26
26
  .synapse/*-wal
27
27
  .synapse/*-shm
28
+ config.json
29
+ status.json
30
+ fix_exc.py
31
+ test_embed.py
@@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [2.1.5] - 2026-06-11
8
+ ## [2.1.7] - 2026-07-29
9
+
10
+ ### Added
11
+ - **AI Engineer Rules**: Created standardized agent rulesets across `AGENTS.md`, `.cursorrules`, `.windsurfrules`, and `.github/copilot-instructions.md`.
12
+
13
+ ### Fixed
14
+ - **Daemon Branch Switching Test Stability**: Resolved timing race condition in daemon branch checkout verification.
15
+ - **CLI Setup Connectivity**: Fixed setup connectivity verification and non-TTY prompt fallback handling.
9
16
 
10
17
  ## [2.1.6] - 2026-06-29
11
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: Persistent structural context infrastructure for AI coding agents.
5
5
  Project-URL: Homepage, https://github.com/saahilpal/synapse
6
6
  Project-URL: Repository, https://github.com/saahilpal/synapse
@@ -339,8 +339,8 @@ Initialize the local SQLite database index:
339
339
  synap init .
340
340
  ```
341
341
 
342
- ### 3. Detach Watcher Daemon
343
- Start background monitoring:
342
+ ### 3. Connect IDE / Start MCP
343
+ Launch the unified background daemon and MCP stdio server:
344
344
  ```bash
345
345
  synap start .
346
346
  ```
@@ -372,7 +372,6 @@ All commands support target path arguments:
372
372
  * `synap doctor [PATH]` — Verifies database integrity and parsers.
373
373
  * `synap run [PATH]` — Runs daemon in foreground.
374
374
  * `synap ui [PATH]` — Launches HTML dashboard.
375
- * `synap mcp start [PATH]` — Hosts MCP stdio server.
376
375
  * `synap mcp config [PATH]` — Outputs Cursor connection blocks.
377
376
  * `synap mcp verify [PATH]` — Asserts MCP server stability.
378
377
  * `synap memory status [PATH]` — Lists lesson counts.
@@ -432,8 +431,7 @@ synap mcp config .
432
431
  "mcpServers": {
433
432
  "synap": {
434
433
  "command": "/usr/local/bin/python",
435
- "args": ["-m", "synap_git.cli", "mcp", "start", "/Users/username/repo"],
436
- "autoConnect": true
434
+ "args": ["-m", "synap_git.cli", "start", "/Users/username/repo"]
437
435
  }
438
436
  }
439
437
  }
@@ -289,8 +289,8 @@ Initialize the local SQLite database index:
289
289
  synap init .
290
290
  ```
291
291
 
292
- ### 3. Detach Watcher Daemon
293
- Start background monitoring:
292
+ ### 3. Connect IDE / Start MCP
293
+ Launch the unified background daemon and MCP stdio server:
294
294
  ```bash
295
295
  synap start .
296
296
  ```
@@ -322,7 +322,6 @@ All commands support target path arguments:
322
322
  * `synap doctor [PATH]` — Verifies database integrity and parsers.
323
323
  * `synap run [PATH]` — Runs daemon in foreground.
324
324
  * `synap ui [PATH]` — Launches HTML dashboard.
325
- * `synap mcp start [PATH]` — Hosts MCP stdio server.
326
325
  * `synap mcp config [PATH]` — Outputs Cursor connection blocks.
327
326
  * `synap mcp verify [PATH]` — Asserts MCP server stability.
328
327
  * `synap memory status [PATH]` — Lists lesson counts.
@@ -382,8 +381,7 @@ synap mcp config .
382
381
  "mcpServers": {
383
382
  "synap": {
384
383
  "command": "/usr/local/bin/python",
385
- "args": ["-m", "synap_git.cli", "mcp", "start", "/Users/username/repo"],
386
- "autoConnect": true
384
+ "args": ["-m", "synap_git.cli", "start", "/Users/username/repo"]
387
385
  }
388
386
  }
389
387
  }
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.1.6"
5
+ __version__ = "2.1.7"
@@ -104,7 +104,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
104
104
  except Exception as e:
105
105
  import structlog
106
106
 
107
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
107
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
108
108
 
109
109
  wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
110
110
  exists = await asyncio.to_thread(wiki_path.exists)
@@ -57,7 +57,7 @@ def _custom_call(*args: Any, **kwargs: Any) -> Any:
57
57
  app.__call__ = _custom_call # type: ignore[method-assign]
58
58
 
59
59
  JSON_OPTION = typer.Option("--json", help="Emit machine-readable JSON.")
60
- console = Console()
60
+ console = Console(stderr=True)
61
61
 
62
62
 
63
63
  def _settings(
@@ -88,9 +88,19 @@ def _settings(
88
88
  return settings
89
89
 
90
90
 
91
+ def _verify_is_git(path: str) -> None:
92
+ from synap_git.git.state import GitRepository
93
+
94
+ if not GitRepository(Path(path)).state().is_repository:
95
+ console.print(f"[bold red]Error:[/bold red] '{path}' is not a valid Git repository.")
96
+ raise typer.Exit(1)
97
+
98
+
91
99
  def _emit(value: Any, *, json_output: bool) -> None:
92
100
  if json_output:
93
- console.print(json.dumps(_jsonable(value), indent=2, sort_keys=True))
101
+ import sys
102
+
103
+ print(json.dumps(_jsonable(value), indent=2, sort_keys=True), file=sys.stdout)
94
104
  return
95
105
  if isinstance(value, str):
96
106
  console.print(value)
@@ -529,6 +539,7 @@ def init(
529
539
  synap init
530
540
  synap init --force --skip-llm
531
541
  """
542
+ _verify_is_git(path)
532
543
  settings = _settings(path, json_output=json_output)
533
544
  if skip_llm:
534
545
  settings.llm_provider = None
@@ -537,12 +548,9 @@ def init(
537
548
 
538
549
  runtime = SynapRuntime(settings)
539
550
  if not quiet and not json_output:
540
- with console.status(
541
- "[yellow]Initializing Synap runtime and scanning repository...[/yellow]", spinner="dots"
542
- ):
543
- commit = runtime.bootstrap(force=force)
544
- else:
545
- commit = runtime.bootstrap(force=force)
551
+ console.print("[yellow]Initializing Synap runtime and scanning repository...[/yellow]")
552
+
553
+ commit = runtime.bootstrap(force=force)
546
554
 
547
555
  if json_output:
548
556
  _emit({"active_commit": commit, "state": "initialized"}, json_output=True)
@@ -646,14 +654,19 @@ def start(
646
654
  path: Annotated[str, typer.Argument(help="Repository path to watch.")] = ".",
647
655
  ) -> None:
648
656
  """Start the Synap daemon in the background."""
657
+ _verify_is_git(path)
658
+ import asyncio
659
+ import os
649
660
  import subprocess
650
661
  import sys
651
662
  import time
652
663
 
664
+ from synap_git.mcp.server import SynapMCPServer
665
+
653
666
  abs_path = Path(path).resolve()
654
667
  pid_file = abs_path / ".synap" / "daemon.pid"
655
668
 
656
- # 1. Check if daemon is already running for the repository
669
+ is_running = False
657
670
  if pid_file.exists():
658
671
  try:
659
672
  pid = int(pid_file.read_text(encoding="utf-8").strip())
@@ -662,59 +675,78 @@ def start(
662
675
  hb = _read_daemon_heartbeat(abs_path)
663
676
  if hb and "port" in hb:
664
677
  console.print(f"[green]✓ UI available at http://127.0.0.1:{hb['port']}[/green]")
665
- return
678
+ is_running = True
666
679
  else:
667
680
  pid_file.unlink()
668
681
  except Exception as e:
669
682
  import structlog
670
683
 
671
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
684
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
672
685
 
673
- # 2. Spawn detached daemon process
674
- cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
675
- try:
676
- if sys.platform == "win32":
677
- subprocess.Popen(
678
- cmd,
679
- stdout=subprocess.DEVNULL,
680
- stderr=subprocess.DEVNULL,
681
- stdin=subprocess.DEVNULL,
682
- creationflags=0x00000008, # DETACHED_PROCESS
683
- )
684
- else:
685
- subprocess.Popen(
686
- cmd,
687
- stdout=subprocess.DEVNULL,
688
- stderr=subprocess.DEVNULL,
689
- stdin=subprocess.DEVNULL,
690
- start_new_session=True,
691
- )
692
- except Exception as e:
693
- console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
694
- raise typer.Exit(1)
686
+ if not is_running:
687
+ # 2. Spawn detached daemon process
688
+ cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
689
+ try:
690
+ if sys.platform == "win32":
691
+ subprocess.Popen(
692
+ cmd,
693
+ stdout=subprocess.DEVNULL,
694
+ stderr=subprocess.DEVNULL,
695
+ stdin=subprocess.DEVNULL,
696
+ creationflags=0x00000008, # DETACHED_PROCESS
697
+ )
698
+ else:
699
+ subprocess.Popen(
700
+ cmd,
701
+ stdout=subprocess.DEVNULL,
702
+ stderr=subprocess.DEVNULL,
703
+ stdin=subprocess.DEVNULL,
704
+ start_new_session=True,
705
+ )
706
+ except Exception as e:
707
+ console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
708
+ raise typer.Exit(1)
695
709
 
696
- success = False
697
- with console.status(
698
- "[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
699
- ):
700
- for _ in range(15): # wait up to 3 seconds
701
- time.sleep(0.2)
702
- if pid_file.exists():
703
- hb = _read_daemon_heartbeat(abs_path)
704
- if hb and hb.get("status") in ("healthy", "degraded"):
705
- success = True
706
- port = hb.get("port", 9876)
707
- pid = int(hb.get("pid", 0))
708
- console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
709
- console.print("[green]✓ Runtime healthy[/green]")
710
- console.print(f"[green]✓ UI available at http://127.0.0.1:{port}[/green]")
711
- break
710
+ success = False
711
+ with console.status(
712
+ "[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
713
+ ):
714
+ for _ in range(25): # wait up to 5 seconds for initial heartbeat
715
+ time.sleep(0.2)
716
+ if pid_file.exists():
717
+ hb = _read_daemon_heartbeat(abs_path)
718
+ if hb:
719
+ status = hb.get("status")
720
+ if status in ("healthy", "degraded", "starting"):
721
+ success = True
722
+ port = hb.get("port", 9876)
723
+ pid = int(hb.get("pid", 0))
724
+ console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
725
+ if status == "starting":
726
+ console.print(
727
+ "[yellow]ℹ Daemon is currently indexing in the background...[/yellow]"
728
+ )
729
+ else:
730
+ console.print("[green]✓ Runtime healthy[/green]")
731
+ console.print(
732
+ f"[green]✓ UI available at http://127.0.0.1:{port}[/green]"
733
+ )
734
+ break
712
735
 
713
- if not success:
714
- console.print(
715
- "[red]✗ Daemon started but did not report healthy status. Check logs in ~/.config/synap/logs/[/red]"
716
- )
717
- raise typer.Exit(1)
736
+ if not success:
737
+ console.print(
738
+ "[red]✗ Daemon started but did not report healthy status. Check logs in ~/.config/synap/logs/[/red]"
739
+ )
740
+ raise typer.Exit(1)
741
+
742
+ # 3. Start MCP Server in the foreground
743
+ settings = _settings(path, json_output=True)
744
+ runtime = SynapRuntime(settings)
745
+ server = SynapMCPServer(runtime)
746
+ try:
747
+ asyncio.run(server.run())
748
+ except KeyboardInterrupt:
749
+ os._exit(0)
718
750
 
719
751
 
720
752
  @app.command("daemon-run", hidden=True)
@@ -820,7 +852,7 @@ def stop(
820
852
  except Exception as e:
821
853
  import structlog
822
854
 
823
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
855
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
824
856
  success = not _is_process_running(pid)
825
857
 
826
858
  # Clean up lockfiles
@@ -1271,7 +1303,7 @@ def rollback(
1271
1303
  except Exception as e:
1272
1304
  import structlog
1273
1305
 
1274
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
1306
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
1275
1307
 
1276
1308
  with console.status(
1277
1309
  f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
@@ -1346,7 +1378,7 @@ def repair(
1346
1378
  except Exception as e:
1347
1379
  import structlog
1348
1380
 
1349
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
1381
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
1350
1382
 
1351
1383
  status_info = runtime.status()
1352
1384
  console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
@@ -1475,6 +1507,12 @@ def doctor(
1475
1507
  completed=1,
1476
1508
  description=f"[green]✓ Daemon active and healthy (PID {daemon_info['pid']}, uptime {daemon_info['uptime_seconds']}s)[/green]",
1477
1509
  )
1510
+ elif status_str == "STARTING":
1511
+ progress.update(
1512
+ task_daemon,
1513
+ completed=1,
1514
+ description=f"[green]✓ Daemon is active and currently starting/indexing (PID {daemon_info['pid']})[/green]",
1515
+ )
1478
1516
  elif status_str == "STALE":
1479
1517
  progress.update(
1480
1518
  task_daemon,
@@ -1509,21 +1547,6 @@ def run(
1509
1547
  start(path)
1510
1548
 
1511
1549
 
1512
- @mcp_app.command("start")
1513
- def mcp_start(
1514
- path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
1515
- ) -> None:
1516
- """Start the MCP server."""
1517
- settings = _settings(path, json_output=True)
1518
- runtime = SynapRuntime(settings)
1519
- runtime.bootstrap()
1520
-
1521
- from synap_git.mcp.server import SynapMCPServer
1522
-
1523
- server = SynapMCPServer(runtime)
1524
- asyncio.run(server.run())
1525
-
1526
-
1527
1550
  @mcp_app.command("config")
1528
1551
  def mcp_config(
1529
1552
  path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
@@ -1534,11 +1557,7 @@ def mcp_config(
1534
1557
 
1535
1558
  config = {
1536
1559
  "mcpServers": {
1537
- "synap": {
1538
- "command": sys.executable,
1539
- "args": ["-m", "synap_git.cli", "mcp", "start", abs_path],
1540
- "autoConnect": True,
1541
- }
1560
+ "synap": {"command": sys.executable, "args": ["-m", "synap_git.cli", "start", abs_path]}
1542
1561
  }
1543
1562
  }
1544
1563
  typer.echo(json.dumps(config, indent=2))
@@ -50,7 +50,6 @@ class SynapSettings(BaseSettings):
50
50
  repository_path: Path = Field(default=Path(), description="Repository root.")
51
51
  state_path: Path = Field(default=Path(".synap"), description="Local runtime state.")
52
52
  sqlite_path: Path | None = None
53
- object_path: Path | None = None
54
53
  log_path: Path | None = None
55
54
 
56
55
  logging_mode: LoggingMode = LoggingMode.HUMAN
@@ -150,7 +149,6 @@ class SynapSettings(BaseSettings):
150
149
  self.repository_path = repository_path
151
150
  self.state_path = state_path.resolve()
152
151
  self.sqlite_path = self._resolve_optional_path(self.sqlite_path, "synap.db")
153
- self.object_path = self._resolve_optional_path(self.object_path, "objects")
154
152
  self.log_path = self._resolve_optional_path(self.log_path, "logs")
155
153
 
156
154
  if self.profile is RuntimeProfile.TEST:
@@ -166,10 +164,6 @@ class SynapSettings(BaseSettings):
166
164
 
167
165
  def ensure_directories(self) -> None:
168
166
  self.state_path.mkdir(parents=True, exist_ok=True)
169
- if self.object_path:
170
- self.object_path.mkdir(parents=True, exist_ok=True)
171
- if self.log_path:
172
- self.log_path.mkdir(parents=True, exist_ok=True)
173
167
 
174
168
  def validate_configuration(self) -> list[str]:
175
169
  errors = []
@@ -56,6 +56,10 @@ def configure_logging(settings: SynapSettings) -> None:
56
56
  # Set up stream handler (stderr)
57
57
  stream_handler = logging.StreamHandler(sys.stderr)
58
58
  stream_handler.setFormatter(logging.Formatter("%(message)s"))
59
+ if settings.log_level.upper() == "DEBUG":
60
+ stream_handler.setLevel(logging.DEBUG)
61
+ else:
62
+ stream_handler.setLevel(logging.WARNING)
59
63
  root_logger.addHandler(stream_handler)
60
64
 
61
65
  root_logger.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO))
@@ -131,7 +131,7 @@ class GitRepository:
131
131
  except Exception as e:
132
132
  import structlog
133
133
 
134
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
134
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
135
135
 
136
136
  if len(current.commit_parent_hashes) > 1:
137
137
  return GitChange(kind=GitChangeKind.MERGE, previous=previous, current=current)
@@ -75,6 +75,7 @@ class RuntimeDaemon:
75
75
  self._ui_server.capture_signals = dummy_capture_signals # type: ignore[method-assign]
76
76
 
77
77
  # Initial bootstrap
78
+ self._write_heartbeat(status="starting")
78
79
  await asyncio.to_thread(self.runtime.bootstrap)
79
80
 
80
81
  with self.runtime.store.connect() as conn:
@@ -128,18 +129,6 @@ class RuntimeDaemon:
128
129
  self.logger.info("daemon_stop_requested")
129
130
  self._stop_event.set()
130
131
 
131
- # Force immediate cleanup and exit to avoid thread-join blocking
132
- if force_exit:
133
- try:
134
- self.runtime.shutdown()
135
- self._delete_heartbeat()
136
- except Exception:
137
- pass
138
-
139
- import sys
140
-
141
- sys.exit(0)
142
-
143
132
  def health(self) -> DaemonHealth:
144
133
  status = self.runtime.status()
145
134
  return DaemonHealth(
@@ -246,7 +235,7 @@ class RuntimeDaemon:
246
235
  }
247
236
  heartbeat_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
248
237
  except Exception as e:
249
- self.logger.error("daemon_write_heartbeat_error", exc_info=True)
238
+ self.logger.error("daemon_write_heartbeat_error", error=str(e))
250
239
 
251
240
  def _delete_heartbeat(self) -> None:
252
241
  heartbeat_file = self.settings.repository_path / ".synap" / "daemon_heartbeat.json"
@@ -256,7 +245,7 @@ class RuntimeDaemon:
256
245
  except Exception as e:
257
246
  import structlog
258
247
 
259
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
248
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
260
249
 
261
250
  # Cleanup process PID lockfile as well
262
251
  pid_file = self.settings.repository_path / ".synap" / "daemon.pid"
@@ -266,7 +255,7 @@ class RuntimeDaemon:
266
255
  except Exception as e:
267
256
  import structlog
268
257
 
269
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
258
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
270
259
 
271
260
  async def _poll_git_loop(self) -> None:
272
261
  while not self._stop_event.is_set():
@@ -302,7 +291,7 @@ class RuntimeDaemon:
302
291
 
303
292
  self._write_heartbeat(status="healthy")
304
293
  except Exception as e:
305
- self.logger.error("daemon_loop_error", exc_info=True)
294
+ self.logger.error("daemon_loop_error", error=str(e))
306
295
  self._recovery_attempts += 1
307
296
  self._write_heartbeat(status="degraded", last_error=str(e))
308
297
 
@@ -334,11 +323,6 @@ class RuntimeDaemon:
334
323
 
335
324
  async def _wiki_worker_loop(self) -> None:
336
325
  self.logger.info("wiki_worker_started")
337
- from rich.console import Console
338
-
339
- from synap_git.config import LoggingMode
340
-
341
- console = Console()
342
326
 
343
327
  while not self._stop_event.is_set():
344
328
  try:
@@ -364,39 +348,7 @@ class RuntimeDaemon:
364
348
 
365
349
  start_time = time.time()
366
350
  try:
367
- if self.settings.logging_mode == LoggingMode.HUMAN:
368
- with console.status(
369
- f"[bold cyan][Wiki][/bold cyan] Generating: [white]{file_path}[/white] (0.0s) ({total_pending} remaining)"
370
- ) as status:
371
- # Update status periodically with elapsed time
372
- async def _update_status(
373
- st: float = start_time,
374
- fp: str = file_path,
375
- tp: int = total_pending,
376
- ) -> None:
377
- try:
378
- while True:
379
- await asyncio.sleep(0.1)
380
- elapsed = time.time() - st
381
- status.update(
382
- f"[bold cyan][Wiki][/bold cyan] Generating: [white]{fp}[/white] [dim]({elapsed:.1f}s)[/dim] ({tp} remaining)"
383
- )
384
- except asyncio.CancelledError:
385
- pass
386
-
387
- status_task = asyncio.create_task(_update_status())
388
- try:
389
- await asyncio.to_thread(
390
- self.runtime.wiki.ensure_wiki_page, file_path
391
- )
392
- finally:
393
- status_task.cancel()
394
- try:
395
- await status_task
396
- except asyncio.CancelledError:
397
- pass
398
- else:
399
- await asyncio.to_thread(self.runtime.wiki.ensure_wiki_page, file_path)
351
+ await asyncio.to_thread(self.runtime.wiki.ensure_wiki_page, file_path)
400
352
 
401
353
  await asyncio.to_thread(
402
354
  self.runtime.store.update_wiki_queue_status,
@@ -408,11 +360,6 @@ class RuntimeDaemon:
408
360
  self.logger.info(
409
361
  "wiki_worker_completed_task", path=file_path, elapsed=elapsed
410
362
  )
411
-
412
- if self.settings.logging_mode == LoggingMode.HUMAN:
413
- print(
414
- f"[bold green]✓[/bold green] Wiki generated: [white]{file_path}[/white] [dim]({elapsed:.1f}s)[/dim]"
415
- )
416
363
  except Exception as ex:
417
364
  self.logger.error("wiki_worker_task_failed", path=file_path, error=str(ex))
418
365
  if attempts + 1 >= 3:
@@ -422,10 +369,6 @@ class RuntimeDaemon:
422
369
  "failed",
423
370
  attempts + 1,
424
371
  )
425
- if self.settings.logging_mode == LoggingMode.HUMAN:
426
- print(
427
- f"[bold red]✗[/bold red] Wiki failed: [white]{file_path}[/white] [dim]({str(ex)})[/dim]"
428
- )
429
372
  else:
430
373
  await asyncio.to_thread(
431
374
  self.runtime.store.update_wiki_queue_status,
@@ -438,6 +381,6 @@ class RuntimeDaemon:
438
381
  except asyncio.CancelledError:
439
382
  break
440
383
  except Exception as e:
441
- self.logger.error("wiki_worker_loop_error", exc_info=True)
384
+ self.logger.error("wiki_worker_loop_error", error=str(e))
442
385
  await asyncio.sleep(5.0)
443
386
  self.logger.info("wiki_worker_stopped")