synap-git 2.1.6__tar.gz → 2.2.0__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.2.0}/.gitignore +7 -0
  2. {synap_git-2.1.6 → synap_git-2.2.0}/CHANGELOG.md +24 -1
  3. {synap_git-2.1.6 → synap_git-2.2.0}/PKG-INFO +6 -7
  4. {synap_git-2.1.6 → synap_git-2.2.0}/README.md +3 -5
  5. {synap_git-2.1.6 → synap_git-2.2.0}/pyproject.toml +1 -0
  6. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/__init__.py +1 -1
  7. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/api/app.py +1 -1
  8. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/main.py +149 -94
  9. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/config.py +0 -6
  10. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/diagnostics/logger.py +4 -0
  11. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/git/state.py +1 -1
  12. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/daemon.py +107 -107
  13. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/engine.py +89 -59
  14. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/scanner.py +2 -2
  15. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/wiki.py +4 -4
  16. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/mcp/server.py +2 -2
  17. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/parser/registry.py +47 -1
  18. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/anthropic.py +6 -5
  19. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/factory.py +4 -3
  20. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/openrouter.py +6 -2
  21. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/retrieval/engine.py +17 -10
  22. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/storage/sqlite.py +38 -1
  23. synap_git-2.2.0/website/README.md +36 -0
  24. synap_git-2.1.6/src/synap_git/cli/mcp.py +0 -137
  25. {synap_git-2.1.6 → synap_git-2.2.0}/.synap.example/README.md +0 -0
  26. {synap_git-2.1.6 → synap_git-2.2.0}/LICENSE.md +0 -0
  27. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/api/__init__.py +0 -0
  28. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/api/static/index.html +0 -0
  29. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/__init__.py +0 -0
  30. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/__main__.py +0 -0
  31. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/checkpoint.py +0 -0
  32. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/lessons.py +0 -0
  33. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/memory.py +0 -0
  34. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/usage.py +0 -0
  35. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/wiki.py +0 -0
  36. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/diagnostics/__init__.py +0 -0
  37. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/diagnostics/tracing.py +0 -0
  38. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/embeddings/__init__.py +0 -0
  39. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/git/__init__.py +0 -0
  40. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/__init__.py +0 -0
  41. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/mcp/__init__.py +0 -0
  42. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/parser/__init__.py +0 -0
  43. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/__init__.py +0 -0
  44. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/base.py +0 -0
  45. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/gemini.py +0 -0
  46. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/ollama.py +0 -0
  47. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/openai.py +0 -0
  48. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/py.typed +0 -0
  49. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/retrieval/__init__.py +0 -0
  50. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/retrieval/memory.py +0 -0
  51. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/__init__.py +0 -0
  52. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/redactor.py +0 -0
  53. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/sanitizer.py +0 -0
  54. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/validator.py +0 -0
  55. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/storage/__init__.py +0 -0
  56. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/utils/__init__.py +0 -0
  57. {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/utils/serialization.py +0 -0
@@ -25,3 +25,10 @@ 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
32
+ .vscode/
33
+ .idea/
34
+ *.log
@@ -5,7 +5,30 @@ 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.2.0] - 2026-08-03
9
+
10
+ ### Added
11
+ - **AST Symbol Parser Expansion (AUD-02)**: Implemented Tree-sitter AST symbol extraction handlers for C#, PHP, Kotlin, Scala, and C.
12
+ - **Event-Driven File Watcher (AUD-03)**: Replaced continuous 2s subprocess polling with `watchdog` filesystem event tracking, reducing idle CPU usage to near-zero.
13
+ - **Daemon REST API Diagnostic Checks (AUD-06)**: Added daemon port availability and REST API endpoint status checks to `synap doctor`.
14
+ - **Wiki Task Requeue Subcommand (AUD-08)**: Added `synap wiki retry` CLI subcommand and `retry_failed_wiki_queue` storage method to requeue permanently failed wiki tasks back to pending state.
15
+
16
+ ### Changed
17
+ - **CLI `synap start` Control Flow (AUD-01)**: Fixed `synap start` so it exits cleanly when a daemon is already running unless `--foreground` is explicitly passed.
18
+ - **SQLite WAL Performance Rationale (AUD-07)**: Documented `PRAGMA synchronous=NORMAL` in WAL mode as an intentional architectural tradeoff balancing write throughput and self-healing index recovery.
19
+
20
+ ### Fixed
21
+ - **Graceful Embedding Provider Fallback (AUD-04)**: Configured Anthropic and OpenRouter providers to log a warning and return empty embedding vectors instead of raising unhandled `NotImplementedError`.
22
+ - **Symbol Preservation on File Renames (AUD-05)**: Added `-M` rename detection to `git diff-tree` in incremental indexing to preserve file symbol history and edges across renames.
23
+
24
+ ## [2.1.7] - 2026-07-29
25
+
26
+ ### Added
27
+ - **AI Engineer Rules**: Created standardized agent rulesets across `AGENTS.md`, `.cursorrules`, `.windsurfrules`, and `.github/copilot-instructions.md`.
28
+
29
+ ### Fixed
30
+ - **Daemon Branch Switching Test Stability**: Resolved timing race condition in daemon branch checkout verification.
31
+ - **CLI Setup Connectivity**: Fixed setup connectivity verification and non-TTY prompt fallback handling.
9
32
 
10
33
  ## [2.1.6] - 2026-06-29
11
34
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: synap-git
3
- Version: 2.1.6
3
+ Version: 2.2.0
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
@@ -37,6 +37,7 @@ Requires-Dist: tree-sitter-languages==1.10.2
37
37
  Requires-Dist: tree-sitter==0.20.4
38
38
  Requires-Dist: typer==0.15.1
39
39
  Requires-Dist: uvicorn==0.34.0
40
+ Requires-Dist: watchdog>=6.0.0
40
41
  Provides-Extra: dev
41
42
  Requires-Dist: bandit==1.8.3; extra == 'dev'
42
43
  Requires-Dist: mypy==1.15.0; extra == 'dev'
@@ -339,8 +340,8 @@ Initialize the local SQLite database index:
339
340
  synap init .
340
341
  ```
341
342
 
342
- ### 3. Detach Watcher Daemon
343
- Start background monitoring:
343
+ ### 3. Connect IDE / Start MCP
344
+ Launch the unified background daemon and MCP stdio server:
344
345
  ```bash
345
346
  synap start .
346
347
  ```
@@ -372,7 +373,6 @@ All commands support target path arguments:
372
373
  * `synap doctor [PATH]` — Verifies database integrity and parsers.
373
374
  * `synap run [PATH]` — Runs daemon in foreground.
374
375
  * `synap ui [PATH]` — Launches HTML dashboard.
375
- * `synap mcp start [PATH]` — Hosts MCP stdio server.
376
376
  * `synap mcp config [PATH]` — Outputs Cursor connection blocks.
377
377
  * `synap mcp verify [PATH]` — Asserts MCP server stability.
378
378
  * `synap memory status [PATH]` — Lists lesson counts.
@@ -432,8 +432,7 @@ synap mcp config .
432
432
  "mcpServers": {
433
433
  "synap": {
434
434
  "command": "/usr/local/bin/python",
435
- "args": ["-m", "synap_git.cli", "mcp", "start", "/Users/username/repo"],
436
- "autoConnect": true
435
+ "args": ["-m", "synap_git.cli", "start", "/Users/username/repo"]
437
436
  }
438
437
  }
439
438
  }
@@ -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
  }
@@ -37,6 +37,7 @@ dependencies = [
37
37
  "tree-sitter-languages==1.10.2",
38
38
  "typer==0.15.1",
39
39
  "uvicorn==0.34.0",
40
+ "watchdog>=6.0.0",
40
41
  ]
41
42
 
42
43
  [project.urls]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.1.6"
5
+ __version__ = "2.2.0"
@@ -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)
@@ -644,16 +652,27 @@ def _detect_install_method() -> str:
644
652
  @app.command()
645
653
  def start(
646
654
  path: Annotated[str, typer.Argument(help="Repository path to watch.")] = ".",
655
+ foreground: Annotated[
656
+ bool,
657
+ typer.Option(
658
+ "--foreground", help="Run FastMCP stdio server in foreground after starting daemon."
659
+ ),
660
+ ] = False,
647
661
  ) -> None:
648
662
  """Start the Synap daemon in the background."""
663
+ _verify_is_git(path)
664
+ import asyncio
665
+ import os
649
666
  import subprocess
650
667
  import sys
651
668
  import time
652
669
 
670
+ from synap_git.mcp.server import SynapMCPServer
671
+
653
672
  abs_path = Path(path).resolve()
654
673
  pid_file = abs_path / ".synap" / "daemon.pid"
655
674
 
656
- # 1. Check if daemon is already running for the repository
675
+ is_running = False
657
676
  if pid_file.exists():
658
677
  try:
659
678
  pid = int(pid_file.read_text(encoding="utf-8").strip())
@@ -662,59 +681,83 @@ def start(
662
681
  hb = _read_daemon_heartbeat(abs_path)
663
682
  if hb and "port" in hb:
664
683
  console.print(f"[green]✓ UI available at http://127.0.0.1:{hb['port']}[/green]")
665
- return
684
+ is_running = True
685
+ if not foreground:
686
+ return
666
687
  else:
667
688
  pid_file.unlink()
668
689
  except Exception as e:
669
690
  import structlog
670
691
 
671
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
692
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
672
693
 
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,
694
+ if not is_running:
695
+ # 2. Spawn detached daemon process
696
+ cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
697
+ try:
698
+ if sys.platform == "win32":
699
+ subprocess.Popen(
700
+ cmd,
701
+ stdout=subprocess.DEVNULL,
702
+ stderr=subprocess.DEVNULL,
703
+ stdin=subprocess.DEVNULL,
704
+ creationflags=0x00000008, # DETACHED_PROCESS
705
+ )
706
+ else:
707
+ subprocess.Popen(
708
+ cmd,
709
+ stdout=subprocess.DEVNULL,
710
+ stderr=subprocess.DEVNULL,
711
+ stdin=subprocess.DEVNULL,
712
+ start_new_session=True,
713
+ )
714
+ except Exception as e:
715
+ console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
716
+ raise typer.Exit(1)
717
+
718
+ success = False
719
+ with console.status(
720
+ "[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
721
+ ):
722
+ for _ in range(25): # wait up to 5 seconds for initial heartbeat
723
+ time.sleep(0.2)
724
+ if pid_file.exists():
725
+ hb = _read_daemon_heartbeat(abs_path)
726
+ if hb:
727
+ status = hb.get("status")
728
+ if status in ("healthy", "degraded", "starting"):
729
+ success = True
730
+ port = hb.get("port", 9876)
731
+ pid = int(hb.get("pid", 0))
732
+ console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
733
+ if status == "starting":
734
+ console.print(
735
+ "[yellow]ℹ Daemon is currently indexing in the background...[/yellow]"
736
+ )
737
+ else:
738
+ console.print("[green]✓ Runtime healthy[/green]")
739
+ console.print(
740
+ f"[green]✓ UI available at http://127.0.0.1:{port}[/green]"
741
+ )
742
+ break
743
+
744
+ if not success:
745
+ console.print(
746
+ "[red]✗ Daemon started but did not report healthy status. Check logs in ~/.config/synap/logs/[/red]"
691
747
  )
692
- except Exception as e:
693
- console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
694
- raise typer.Exit(1)
748
+ raise typer.Exit(1)
695
749
 
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
750
+ if not foreground:
751
+ return
712
752
 
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)
753
+ # 3. Start MCP Server in the foreground if --foreground flag passed
754
+ settings = _settings(path, json_output=True)
755
+ runtime = SynapRuntime(settings)
756
+ server = SynapMCPServer(runtime)
757
+ try:
758
+ asyncio.run(server.run())
759
+ except KeyboardInterrupt:
760
+ os._exit(0)
718
761
 
719
762
 
720
763
  @app.command("daemon-run", hidden=True)
@@ -820,7 +863,7 @@ def stop(
820
863
  except Exception as e:
821
864
  import structlog
822
865
 
823
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
866
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
824
867
  success = not _is_process_running(pid)
825
868
 
826
869
  # Clean up lockfiles
@@ -1271,7 +1314,7 @@ def rollback(
1271
1314
  except Exception as e:
1272
1315
  import structlog
1273
1316
 
1274
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
1317
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
1275
1318
 
1276
1319
  with console.status(
1277
1320
  f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
@@ -1346,7 +1389,7 @@ def repair(
1346
1389
  except Exception as e:
1347
1390
  import structlog
1348
1391
 
1349
- structlog.get_logger().error("suppressed_error_caught", exc_info=True)
1392
+ structlog.get_logger().error("suppressed_error_caught", error=str(e))
1350
1393
 
1351
1394
  status_info = runtime.status()
1352
1395
  console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
@@ -1464,34 +1507,52 @@ def doctor(
1464
1507
  task_prov, completed=1, description=f"[red]✗ Unexpected error: {e}[/red]"
1465
1508
  )
1466
1509
 
1467
- task_daemon = progress.add_task("Checking Daemon Status...", total=1)
1510
+ task_daemon = progress.add_task("Checking Daemon & REST API Status...", total=1)
1468
1511
  try:
1512
+ import socket
1513
+
1514
+ import httpx
1515
+
1469
1516
  daemon_info = _read_daemon_heartbeat(settings.repository_path)
1470
- if daemon_info:
1471
- status_str = daemon_info["status"].upper()
1472
- if status_str == "HEALTHY":
1473
- progress.update(
1474
- task_daemon,
1475
- completed=1,
1476
- description=f"[green]✓ Daemon active and healthy (PID {daemon_info['pid']}, uptime {daemon_info['uptime_seconds']}s)[/green]",
1477
- )
1478
- elif status_str == "STALE":
1479
- progress.update(
1480
- task_daemon,
1481
- completed=1,
1482
- description="[yellow]⚠ Daemon heartbeat file exists but is stale (not running?)[/yellow]",
1483
- )
1484
- else:
1517
+ port = daemon_info.get("port", 9876) if daemon_info else 9876
1518
+
1519
+ # Test port availability
1520
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1521
+ s.settimeout(0.5)
1522
+ port_in_use = s.connect_ex(("127.0.0.1", port)) == 0
1523
+
1524
+ if daemon_info and port_in_use:
1525
+ try:
1526
+ r = httpx.get(f"http://127.0.0.1:{port}/api/status", timeout=2.0)
1527
+ if r.status_code == 200:
1528
+ progress.update(
1529
+ task_daemon,
1530
+ completed=1,
1531
+ description=f"[green]✓ Daemon active & REST API healthy (PID {daemon_info['pid']}, port {port})[/green]",
1532
+ )
1533
+ else:
1534
+ progress.update(
1535
+ task_daemon,
1536
+ completed=1,
1537
+ description=f"[yellow]⚠ Daemon running on port {port} but REST API returned HTTP {r.status_code}[/yellow]",
1538
+ )
1539
+ except Exception as ex:
1485
1540
  progress.update(
1486
1541
  task_daemon,
1487
1542
  completed=1,
1488
- description=f"[red]✗ Daemon degraded: {daemon_info.get('last_error')}[/red]",
1543
+ description=f"[yellow]⚠ Daemon running on port {port} but API check failed: {ex}[/yellow]",
1489
1544
  )
1545
+ elif daemon_info:
1546
+ progress.update(
1547
+ task_daemon,
1548
+ completed=1,
1549
+ description=f"[yellow]⚠ Heartbeat found for PID {daemon_info['pid']} but port {port} is not listening[/yellow]",
1550
+ )
1490
1551
  else:
1491
1552
  progress.update(
1492
1553
  task_daemon,
1493
1554
  completed=1,
1494
- description="[yellow]⚠ Daemon inactive (not running). Start it using `synap start`[/yellow]",
1555
+ description=f"[yellow]⚠ Daemon inactive (port {port} available). Start using `synap start`[/yellow]",
1495
1556
  )
1496
1557
  except Exception as e:
1497
1558
  progress.update(
@@ -1509,21 +1570,6 @@ def run(
1509
1570
  start(path)
1510
1571
 
1511
1572
 
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
1573
  @mcp_app.command("config")
1528
1574
  def mcp_config(
1529
1575
  path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
@@ -1534,11 +1580,7 @@ def mcp_config(
1534
1580
 
1535
1581
  config = {
1536
1582
  "mcpServers": {
1537
- "synap": {
1538
- "command": sys.executable,
1539
- "args": ["-m", "synap_git.cli", "mcp", "start", abs_path],
1540
- "autoConnect": True,
1541
- }
1583
+ "synap": {"command": sys.executable, "args": ["-m", "synap_git.cli", "start", abs_path]}
1542
1584
  }
1543
1585
  }
1544
1586
  typer.echo(json.dumps(config, indent=2))
@@ -2154,3 +2196,16 @@ def wiki_show(
2154
2196
 
2155
2197
  content = wiki_path.read_text(encoding="utf-8")
2156
2198
  console.print(Markdown(content))
2199
+
2200
+
2201
+ @wiki_app.command("retry")
2202
+ def wiki_retry(
2203
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
2204
+ ) -> None:
2205
+ """Requeue all permanently failed wiki tasks back to pending state."""
2206
+ runtime = SynapRuntime(_settings(path))
2207
+ count = runtime.store.retry_failed_wiki_queue()
2208
+ if count > 0:
2209
+ console.print(f"[green]✓ Requeued {count} failed wiki tasks to pending state.[/green]")
2210
+ else:
2211
+ console.print("[yellow]No failed wiki tasks found in queue.[/yellow]")
@@ -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)