synap-git 2.1.5__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.5 → synap_git-2.1.7}/.gitignore +4 -0
  2. {synap_git-2.1.5 → synap_git-2.1.7}/CHANGELOG.md +22 -1
  3. {synap_git-2.1.5 → synap_git-2.1.7}/PKG-INFO +20 -18
  4. {synap_git-2.1.5 → synap_git-2.1.7}/README.md +15 -13
  5. {synap_git-2.1.5 → synap_git-2.1.7}/pyproject.toml +4 -4
  6. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/__init__.py +1 -1
  7. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/api/app.py +1 -1
  8. synap_git-2.1.7/src/synap_git/cli/checkpoint.py +125 -0
  9. synap_git-2.1.7/src/synap_git/cli/lessons.py +132 -0
  10. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/cli/main.py +126 -83
  11. synap_git-2.1.7/src/synap_git/cli/memory.py +114 -0
  12. synap_git-2.1.7/src/synap_git/cli/usage.py +88 -0
  13. synap_git-2.1.7/src/synap_git/cli/wiki.py +86 -0
  14. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/config.py +7 -6
  15. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/diagnostics/logger.py +4 -0
  16. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/diagnostics/tracing.py +2 -4
  17. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/git/state.py +1 -1
  18. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/indexer/daemon.py +4 -61
  19. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/indexer/engine.py +148 -120
  20. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/indexer/scanner.py +31 -1
  21. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/indexer/wiki.py +2 -2
  22. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/mcp/server.py +2 -6
  23. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/parser/registry.py +33 -36
  24. synap_git-2.1.7/src/synap_git/provider/__init__.py +1 -0
  25. synap_git-2.1.7/src/synap_git/provider/factory.py +165 -0
  26. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/retrieval/engine.py +12 -7
  27. synap_git-2.1.7/src/synap_git/security/__init__.py +5 -0
  28. synap_git-2.1.7/src/synap_git/security/redactor.py +56 -0
  29. synap_git-2.1.7/src/synap_git/security/sanitizer.py +29 -0
  30. synap_git-2.1.7/src/synap_git/security/validator.py +33 -0
  31. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/storage/sqlite.py +40 -4
  32. synap_git-2.1.7/website/README.md +36 -0
  33. synap_git-2.1.5/src/synap_git/provider/factory.py +0 -107
  34. {synap_git-2.1.5 → synap_git-2.1.7}/.synap.example/README.md +0 -0
  35. {synap_git-2.1.5 → synap_git-2.1.7}/LICENSE.md +0 -0
  36. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/api/__init__.py +0 -0
  37. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/api/static/index.html +0 -0
  38. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/cli/__init__.py +0 -0
  39. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/cli/__main__.py +0 -0
  40. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/diagnostics/__init__.py +0 -0
  41. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/embeddings/__init__.py +0 -0
  42. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/git/__init__.py +0 -0
  43. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/indexer/__init__.py +0 -0
  44. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/mcp/__init__.py +0 -0
  45. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/parser/__init__.py +0 -0
  46. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/provider/anthropic.py +0 -0
  47. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/provider/base.py +0 -0
  48. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/provider/gemini.py +0 -0
  49. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/provider/ollama.py +0 -0
  50. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/provider/openai.py +0 -0
  51. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/provider/openrouter.py +0 -0
  52. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/py.typed +0 -0
  53. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/retrieval/__init__.py +0 -0
  54. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/retrieval/memory.py +0 -0
  55. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/storage/__init__.py +0 -0
  56. {synap_git-2.1.5 → synap_git-2.1.7}/src/synap_git/utils/__init__.py +0 -0
  57. {synap_git-2.1.5 → 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,28 @@ 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.
16
+
17
+ ## [2.1.6] - 2026-06-29
18
+
19
+ ### Added
20
+ - **Universal Language Expansion**: Expanded tree-sitter AST parsing support from 4 hardcoded languages to dynamically supporting over ~50 languages using `tree-sitter-languages`. Also added dynamic shebang detection (`#!`) to parse extensionless scripts as python, ruby, node, and bash scripts.
21
+ - **API Rate Limiting**: Implemented a global `RateLimitedProvider` to throttle LLM and embedding calls to 3 requests per second to mitigate resource exhaustion on the diagnostic API.
22
+
23
+ ### Fixed
24
+ - **Broad Exception Handling**: Eradicated remaining `except Exception:` blocks across diagnostics, daemon execution, indexer scanning, and module resolution, and replaced them with `OSError`, `sqlite3.Error`, and specific errors to prevent silent data corruption.
25
+ - **Async Sleep Compatibility**: Fixed thread-blocking `time.sleep()` in LLM retries by delegating to `anyio.from_thread.run(anyio.sleep)` if invoked inside FastMCP worker threads.
26
+ - **Edge Resolution & Source Paths**: Added support for resolving Javascript, Java, C++, Ruby, and more during import/edge resolution.
27
+ - **Module Path Trimming**: Improved module path mapping by trimming `internal/`, `source/`, `packages/`, `main/`, `java/`, `kotlin/`, `scala/` from module keys.
28
+ - **Wipe Confirmation**: Added a `--yes` confirmation bypass flag for `synap wipe` and enforced safety confirmation in non-interactive terminals.
29
+ - **Metadata Fixes**: Corrected repository URLs in `pyproject.toml` to correctly point to `synapse` instead of `synap-git`.
9
30
 
10
31
  ### Refactored
11
32
  - **Daemon Teardown**: Refactored the forceful SIGTERM daemon teardown logic to safely toggle via `force_exit=True` rather than a blanket catch-all. Makes programmatic `daemon.stop()` safer without blocking terminal UI interactions.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 2.1.5
3
+ Version: 2.1.7
4
4
  Summary: Persistent structural context infrastructure for AI coding agents.
5
- Project-URL: Homepage, https://github.com/saahilpal/synap-git
6
- Project-URL: Repository, https://github.com/saahilpal/synap-git
7
- Project-URL: Issues, https://github.com/saahilpal/synap-git/issues
8
- Project-URL: Changelog, https://github.com/saahilpal/synap-git/blob/main/CHANGELOG.md
5
+ Project-URL: Homepage, https://github.com/saahilpal/synapse
6
+ Project-URL: Repository, https://github.com/saahilpal/synapse
7
+ Project-URL: Issues, https://github.com/saahilpal/synapse/issues
8
+ Project-URL: Changelog, https://github.com/saahilpal/synapse/blob/main/CHANGELOG.md
9
9
  Author: Synap contributors
10
10
  License: Apache-2.0
11
11
  License-File: LICENSE.md
@@ -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
  }
@@ -443,15 +441,19 @@ synap mcp config .
443
441
 
444
442
  ## Supported Languages
445
443
 
446
- Synap processes syntax for the following formats:
444
+ Synap now processes syntax dynamically for **over 50 programming languages** via `tree-sitter-languages`, including:
445
+
447
446
  * **Python** (`.py`)
448
- * **JavaScript / JSX** (`.js`, `.jsx`)
449
- * **TypeScript / TSX** (`.ts`, `.tsx`)
450
- * **Go** (`.go`)
451
- * **Rust** (`.rs`)
452
- * **Java** (`.java`)
453
- * **C / C++** (`.cpp`, `.cc`, `.cxx`, `.hpp`, `.h`)
454
- * **Ruby** (`.rb`)
447
+ * **JavaScript / TypeScript** (`.js`, `.jsx`, `.ts`, `.tsx`)
448
+ * **C / C++ / C#** (`.c`, `.cpp`, `.h`, `.hpp`, `.cs`)
449
+ * **Java / Kotlin / Scala** (`.java`, `.kt`, `.kts`, `.scala`)
450
+ * **Go / Rust / Swift** (`.go`, `.rs`, `.swift`)
451
+ * **Ruby / PHP / Perl** (`.rb`, `.php`, `.pl`)
452
+ * **Shell Scripts** (`.sh`, `.bash`, or via `#!` shebang detection)
453
+ * **Data / Config** (`.sql`, `.json`, `.yaml`, `.toml`, `.xml`)
454
+ * **Web** (`.html`, `.css`, `.scss`)
455
+
456
+ Even extensionless scripts are gracefully detected via shebang parsing. Missing grammars automatically degrade gracefully to plaintext indexing.
455
457
 
456
458
  ---
457
459
 
@@ -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
  }
@@ -393,15 +391,19 @@ synap mcp config .
393
391
 
394
392
  ## Supported Languages
395
393
 
396
- Synap processes syntax for the following formats:
394
+ Synap now processes syntax dynamically for **over 50 programming languages** via `tree-sitter-languages`, including:
395
+
397
396
  * **Python** (`.py`)
398
- * **JavaScript / JSX** (`.js`, `.jsx`)
399
- * **TypeScript / TSX** (`.ts`, `.tsx`)
400
- * **Go** (`.go`)
401
- * **Rust** (`.rs`)
402
- * **Java** (`.java`)
403
- * **C / C++** (`.cpp`, `.cc`, `.cxx`, `.hpp`, `.h`)
404
- * **Ruby** (`.rb`)
397
+ * **JavaScript / TypeScript** (`.js`, `.jsx`, `.ts`, `.tsx`)
398
+ * **C / C++ / C#** (`.c`, `.cpp`, `.h`, `.hpp`, `.cs`)
399
+ * **Java / Kotlin / Scala** (`.java`, `.kt`, `.kts`, `.scala`)
400
+ * **Go / Rust / Swift** (`.go`, `.rs`, `.swift`)
401
+ * **Ruby / PHP / Perl** (`.rb`, `.php`, `.pl`)
402
+ * **Shell Scripts** (`.sh`, `.bash`, or via `#!` shebang detection)
403
+ * **Data / Config** (`.sql`, `.json`, `.yaml`, `.toml`, `.xml`)
404
+ * **Web** (`.html`, `.css`, `.scss`)
405
+
406
+ Even extensionless scripts are gracefully detected via shebang parsing. Missing grammars automatically degrade gracefully to plaintext indexing.
405
407
 
406
408
  ---
407
409
 
@@ -40,10 +40,10 @@ dependencies = [
40
40
  ]
41
41
 
42
42
  [project.urls]
43
- Homepage = "https://github.com/saahilpal/synap-git"
44
- Repository = "https://github.com/saahilpal/synap-git"
45
- Issues = "https://github.com/saahilpal/synap-git/issues"
46
- Changelog = "https://github.com/saahilpal/synap-git/blob/main/CHANGELOG.md"
43
+ Homepage = "https://github.com/saahilpal/synapse"
44
+ Repository = "https://github.com/saahilpal/synapse"
45
+ Issues = "https://github.com/saahilpal/synapse/issues"
46
+ Changelog = "https://github.com/saahilpal/synapse/blob/main/CHANGELOG.md"
47
47
 
48
48
 
49
49
  [project.optional-dependencies]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.1.5"
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", error=str(e), 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)
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.table import Table
10
+
11
+ from synap_git.cli.main import (
12
+ _settings,
13
+ console,
14
+ )
15
+ from synap_git.indexer.engine import SynapRuntime
16
+
17
+ app = typer.Typer(help="Checkpoint commands.", no_args_is_help=True)
18
+
19
+
20
+ @app.command("create")
21
+ def checkpoint_create(
22
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
23
+ doing: Annotated[
24
+ str, typer.Option(help="What the agent is currently doing.")
25
+ ] = "Manual snapshot",
26
+ files: Annotated[str, typer.Option(help="Comma-separated list of changed files.")] = "",
27
+ next_step: Annotated[str, typer.Option(help="The next step to be taken.")] = "",
28
+ blockers: Annotated[str, typer.Option(help="Current blockers or obstacles.")] = "",
29
+ ) -> None:
30
+ """Create a new context checkpoint."""
31
+ runtime = SynapRuntime(_settings(path))
32
+
33
+ checkpoint_id = str(uuid.uuid4())
34
+ status = runtime.status()
35
+ branch = status.branch
36
+ commit = status.git_commit or "unknown"
37
+
38
+ file_list = [f.strip() for f in files.split(",") if f.strip()]
39
+
40
+ runtime.store.put_checkpoint(
41
+ checkpoint_id=checkpoint_id,
42
+ branch=branch,
43
+ commit_hash=commit,
44
+ doing=doing,
45
+ changed_files=json.dumps(file_list),
46
+ next_step=next_step,
47
+ blockers=blockers,
48
+ )
49
+ console.print(f"[green]✓ Checkpoint {checkpoint_id} created for branch '{branch}'.[/green]")
50
+
51
+
52
+ @app.command("list")
53
+ def checkpoint_list(
54
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
55
+ ) -> None:
56
+ """List checkpoints for the active branch."""
57
+ runtime = SynapRuntime(_settings(path))
58
+ status = runtime.status()
59
+ branch = status.branch
60
+ cps = runtime.store.get_checkpoints(branch)
61
+
62
+ if not cps:
63
+ console.print(f"[yellow]No checkpoints found for branch '{branch}'.[/yellow]")
64
+ return
65
+
66
+ table = Table(
67
+ title=f"Checkpoints for branch '{branch}'", show_header=True, header_style="bold cyan"
68
+ )
69
+ table.add_column("Checkpoint ID")
70
+ table.add_column("Commit Hash")
71
+ table.add_column("Doing")
72
+ table.add_column("Changed Files")
73
+ table.add_column("Created At")
74
+
75
+ for cp in cps:
76
+ created = datetime.fromtimestamp(cp["created_at"]).isoformat()
77
+ try:
78
+ ch_files = ", ".join(json.loads(cp["changed_files"]))
79
+ except Exception:
80
+ ch_files = cp["changed_files"]
81
+ table.add_row(
82
+ cp["checkpoint_id"][:16] + "…",
83
+ cp["commit_hash"][:7],
84
+ cp["doing"],
85
+ ch_files if ch_files else "None",
86
+ created,
87
+ )
88
+ console.print(table)
89
+
90
+
91
+ @app.command("restore")
92
+ def checkpoint_restore(
93
+ checkpoint_id: Annotated[
94
+ str, typer.Argument(help="The ID of the checkpoint to restore (defaults to latest).")
95
+ ] = "latest",
96
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
97
+ ) -> None:
98
+ """Restore and show details of a checkpoint."""
99
+ runtime = SynapRuntime(_settings(path))
100
+ status = runtime.status()
101
+ branch = status.branch
102
+
103
+ if checkpoint_id == "latest":
104
+ cp = runtime.store.get_latest_checkpoint(branch)
105
+ else:
106
+ cp = runtime.store.get_checkpoint(checkpoint_id)
107
+
108
+ if not cp:
109
+ console.print(f"[red]✗ Checkpoint '{checkpoint_id}' not found.[/red]")
110
+ raise typer.Exit(1)
111
+
112
+ console.print(f"[bold green]✓ Checkpoint '{cp['checkpoint_id']}' details:[/bold green]")
113
+ console.print(f" [bold]Branch:[/bold] {cp['branch']}")
114
+ console.print(f" [bold]Commit:[/bold] {cp['commit_hash']}")
115
+ console.print(f" [bold]Doing:[/bold] {cp['doing']}")
116
+ try:
117
+ ch_files = ", ".join(json.loads(cp["changed_files"]))
118
+ except Exception:
119
+ ch_files = cp["changed_files"]
120
+ console.print(f" [bold]Changed Files:[/bold] {ch_files if ch_files else 'None'}")
121
+ console.print(f" [bold]Next Step:[/bold] {cp['next_step'] or 'None'}")
122
+ console.print(f" [bold]Blockers:[/bold] {cp['blockers'] or 'None'}")
123
+ console.print(
124
+ f" [bold]Created At:[/bold] {datetime.fromtimestamp(cp['created_at']).isoformat()}"
125
+ )
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import questionary
6
+ import typer
7
+ from rich.panel import Panel
8
+
9
+ from synap_git.cli.main import (
10
+ _settings,
11
+ console,
12
+ )
13
+ from synap_git.indexer.engine import SynapRuntime
14
+
15
+ app = typer.Typer(help="Lessons commands.", no_args_is_help=True)
16
+
17
+
18
+ @app.command("approve")
19
+ def lessons_approve(
20
+ lesson_id: Annotated[str, typer.Argument(help="The ID of the lesson to approve.")],
21
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
22
+ ) -> None:
23
+ """Approve a pending lesson."""
24
+ runtime = SynapRuntime(_settings(path))
25
+ # Fetch to ensure it exists and is pending
26
+ pending = runtime.store.get_lessons("pending")
27
+ target = next((lesson for lesson in pending if lesson["lesson_id"] == lesson_id), None)
28
+
29
+ if not target:
30
+ console.print(f"[red]✗ Pending lesson {lesson_id} not found.[/red]")
31
+ raise typer.Exit(1)
32
+
33
+ runtime.store.update_lesson(lesson_id, target["why_failed"], "approved", actor="cli_user")
34
+ console.print(f"[green]✓ Lesson {lesson_id} approved. Memory updated.[/green]")
35
+
36
+
37
+ @app.command("review")
38
+ def lessons_review(
39
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
40
+ ) -> None:
41
+ """Interactively review and manage pending lessons."""
42
+
43
+ runtime = SynapRuntime(_settings(path))
44
+ pending = runtime.store.get_lessons("pending")
45
+
46
+ if not pending:
47
+ console.print("[yellow]No pending lessons to review.[/yellow]")
48
+ return
49
+
50
+ console.print(f"[bold cyan]Reviewing {len(pending)} pending lessons...[/bold cyan]\n")
51
+
52
+ approved_count = 0
53
+ rejected_count = 0
54
+ skipped_count = 0
55
+
56
+ for lesson in pending:
57
+ lesson_id = lesson["lesson_id"]
58
+
59
+ # Build detail view
60
+ details = (
61
+ f"[bold]Revert Commit:[/bold] {lesson['revert_commit'][:8]}\n"
62
+ f"[bold]Reverted From:[/bold] {lesson['reverted_from'][:8]}\n"
63
+ f"[bold]Files Affected:[/bold] {lesson['files_affected']}\n"
64
+ f"[bold]What Failed:[/bold] {lesson['what_failed']}\n"
65
+ f"[bold]Proposed Lesson:[/bold] {lesson['why_failed']}"
66
+ )
67
+
68
+ console.print(Panel(details, title=f"Lesson {lesson_id[:8]}", expand=False))
69
+
70
+ choice = questionary.select(
71
+ "Action:",
72
+ choices=[
73
+ questionary.Choice("Approve", value="approve"),
74
+ questionary.Choice("Edit & Approve", value="edit"),
75
+ questionary.Choice("Reject", value="reject"),
76
+ questionary.Choice("Skip", value="skip"),
77
+ ],
78
+ ).ask()
79
+
80
+ if choice == "approve":
81
+ runtime.store.update_lesson(
82
+ lesson_id, lesson["why_failed"], "approved", actor="cli_user"
83
+ )
84
+ console.print("[green]✓ Approved.[/green]\n")
85
+ approved_count += 1
86
+ elif choice == "edit":
87
+ edited = questionary.text("Edit lesson text:", default=lesson["why_failed"]).ask()
88
+ if edited:
89
+ runtime.store.update_lesson(lesson_id, edited, "approved", actor="cli_user")
90
+ console.print("[green]✓ Edited and Approved.[/green]\n")
91
+ approved_count += 1
92
+ else:
93
+ console.print("[yellow]Skipping edit.[/yellow]\n")
94
+ skipped_count += 1
95
+ elif choice == "reject":
96
+ runtime.store.update_lesson(
97
+ lesson_id, lesson["why_failed"], "rejected", actor="cli_user"
98
+ )
99
+ console.print("[red]✗ Rejected.[/red]\n")
100
+ rejected_count += 1
101
+ elif choice == "skip":
102
+ console.print("[yellow]Skipped.[/yellow]\n")
103
+ skipped_count += 1
104
+ else:
105
+ console.print("[yellow]Aborting review.[/yellow]")
106
+ break
107
+
108
+ console.print("[bold cyan]Review Summary:[/bold cyan]")
109
+ console.print(f" [green]Approved:[/green] {approved_count}")
110
+ console.print(f" [red]Rejected:[/red] {rejected_count}")
111
+ console.print(f" [yellow]Skipped:[/yellow] {skipped_count}")
112
+
113
+
114
+ @app.command("reject")
115
+ def lessons_reject(
116
+ lesson_id: Annotated[str, typer.Argument(help="The ID of the lesson to reject.")],
117
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
118
+ ) -> None:
119
+ """Reject a pending or approved lesson."""
120
+ runtime = SynapRuntime(_settings(path))
121
+ pending = runtime.store.get_lessons("pending")
122
+ approved = runtime.store.get_lessons("approved")
123
+ target = next(
124
+ (lesson for lesson in pending + approved if lesson["lesson_id"] == lesson_id), None
125
+ )
126
+
127
+ if not target:
128
+ console.print(f"[red]✗ Lesson {lesson_id} not found in pending or approved queues.[/red]")
129
+ raise typer.Exit(1)
130
+
131
+ runtime.store.update_lesson(lesson_id, target["why_failed"], "rejected", actor="cli_user")
132
+ console.print(f"[yellow]✓ Lesson {lesson_id} rejected. Memory updated.[/yellow]")