loki-mode 7.5.28 → 7.5.30

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 (76) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/loki +3 -3
  4. package/dashboard/__init__.py +1 -1
  5. package/dashboard/server.py +19 -0
  6. package/docs/INSTALLATION.md +1 -1
  7. package/docs/MERGE-DEDUP-MAP.md +420 -0
  8. package/docs/MERGE-ROUTE-MAP.md +139 -0
  9. package/docs/MERGE3-PLAN.md +119 -0
  10. package/loki-ts/dist/loki.js +2 -2
  11. package/mcp/__init__.py +1 -1
  12. package/package.json +2 -2
  13. package/web-app/dist/assets/{AdminPage-DwVUK4v9.js → AdminPage-CKUOsWZW.js} +3 -3
  14. package/web-app/dist/assets/{Avatar-B7gqhcg3.js → Avatar-CL9Id9Hi.js} +1 -1
  15. package/web-app/dist/assets/{Badge-DA3xNJAS.js → Badge-B12zwlD7.js} +1 -1
  16. package/web-app/dist/assets/{Button-BPXURLaK.js → Button-CFLVoduT.js} +1 -1
  17. package/web-app/dist/assets/{ComparePage-B0JQMhKG.js → ComparePage-Dg0UdZAk.js} +1 -1
  18. package/web-app/dist/assets/{GitHubIssuesPanel-D38-fy29.js → GitHubIssuesPanel-CSitxtAX.js} +2 -2
  19. package/web-app/dist/assets/{GitHubPRsPanel-DLPcW3N0.js → GitHubPRsPanel-BIT06FRo.js} +1 -1
  20. package/web-app/dist/assets/HomePage-pU_0fGny.js +28 -0
  21. package/web-app/dist/assets/{LoginPage-DqCzxsfx.js → LoginPage-DTZtt2Yb.js} +1 -1
  22. package/web-app/dist/assets/MagicPage-10zfra8o.js +31 -0
  23. package/web-app/dist/assets/{MetricsPage-CPYQR0zr.js → MetricsPage-C-wiKUkv.js} +1 -1
  24. package/web-app/dist/assets/NotFoundPage-BDkcmhYe.js +1 -0
  25. package/web-app/dist/assets/{ProjectPage-DNujSl6j.js → ProjectPage-CiCavQ8n.js} +71 -71
  26. package/web-app/dist/assets/ProjectsPage-BLCXQwwC.js +6 -0
  27. package/web-app/dist/assets/{SettingsPage-BaQJbOgL.js → SettingsPage-PkxtaMyg.js} +3 -3
  28. package/web-app/dist/assets/{ShowcasePage-DQR_e-kg.js → ShowcasePage-iECp8Tha.js} +1 -1
  29. package/web-app/dist/assets/SystemSettingsPage-DS6Anno1.js +6 -0
  30. package/web-app/dist/assets/{TeamsPage-DOFErDqX.js → TeamsPage-ls6h6bNL.js} +1 -1
  31. package/web-app/dist/assets/{TemplatesPage-Ty72hILN.js → TemplatesPage-Bk0QzlPt.js} +3 -3
  32. package/web-app/dist/assets/{TerminalOutput-DqOVnR1p.js → TerminalOutput-4-1hWCtZ.js} +1 -1
  33. package/web-app/dist/assets/{activity-BgBZ4s4c.js → activity-DH3ih2nS.js} +1 -1
  34. package/web-app/dist/assets/{bell-C-UezVWi.js → bell-Gn17S6uv.js} +1 -1
  35. package/web-app/dist/assets/{bot-D70fEnm5.js → bot-Cbycc3VE.js} +1 -1
  36. package/web-app/dist/assets/{check-CBohulxQ.js → check-nIAqa-kf.js} +1 -1
  37. package/web-app/dist/assets/{chevron-left-C-emzUhB.js → chevron-left-D2jcWDll.js} +1 -1
  38. package/web-app/dist/assets/{circle-alert-8SRY0_GX.js → circle-alert-CpL4Bhvt.js} +1 -1
  39. package/web-app/dist/assets/{clock-mfq4XnPQ.js → clock-IW4Wq86N.js} +1 -1
  40. package/web-app/dist/assets/{cloud-DpRM7T8t.js → cloud-Cn8nNuH2.js} +1 -1
  41. package/web-app/dist/assets/{code-xml-1N2Ui-4c.js → code-xml-BiJBteXf.js} +1 -1
  42. package/web-app/dist/assets/{copy-LXquTgzI.js → copy-CnqkyNsi.js} +1 -1
  43. package/web-app/dist/assets/{database-S1dyXnuT.js → database-CKSReqa5.js} +1 -1
  44. package/web-app/dist/assets/{dollar-sign-CRqk0dW5.js → dollar-sign-CDzDY64R.js} +1 -1
  45. package/web-app/dist/assets/{file-code-corner-B99CwY_6.js → file-code-corner-Box4IwG1.js} +1 -1
  46. package/web-app/dist/assets/{file-plus-DZ5qnz5b.js → file-plus-DpGqlXF8.js} +1 -1
  47. package/web-app/dist/assets/{folder-open-DBCm7yuF.js → folder-open-B57dAoBv.js} +1 -1
  48. package/web-app/dist/assets/{git-commit-horizontal-DM1ERuNd.js → git-commit-horizontal-BVbucmO5.js} +1 -1
  49. package/web-app/dist/assets/{globe-B7xEJSL_.js → globe-BkOnKl4x.js} +1 -1
  50. package/web-app/dist/assets/{hammer-Cgi3LTuS.js → hammer-DRbIQ4QU.js} +1 -1
  51. package/web-app/dist/assets/{index-BN52-GQT.js → index-CM_b_EhP.js} +77 -77
  52. package/web-app/dist/assets/{layers-Bi8RPIBC.js → layers-B78BiFiU.js} +1 -1
  53. package/web-app/dist/assets/{lightbulb-Doc_n8JX.js → lightbulb-B-Itbm9g.js} +1 -1
  54. package/web-app/dist/assets/{loader-circle-BB932A7A.js → loader-circle-Oq6NQhW2.js} +1 -1
  55. package/web-app/dist/assets/{lock-Bt6gpMrs.js → lock-DbJ9zxbw.js} +1 -1
  56. package/web-app/dist/assets/{mail-BuzAu1IP.js → mail-CzMRod6m.js} +1 -1
  57. package/web-app/dist/assets/{package-BE5FHxQ8.js → package-WZ5osvej.js} +1 -1
  58. package/web-app/dist/assets/{plus-CNqABexN.js → plus-j08lFR-K.js} +1 -1
  59. package/web-app/dist/assets/{refresh-cw-34B13ztx.js → refresh-cw-CIr7E-g2.js} +1 -1
  60. package/web-app/dist/assets/{rotate-ccw-CrD2QB29.js → rotate-ccw-gwoXxDeE.js} +1 -1
  61. package/web-app/dist/assets/{save-DsJcqdnI.js → save-B8fV_ZpE.js} +1 -1
  62. package/web-app/dist/assets/{server-BcgRMArA.js → server-D5dO1paz.js} +1 -1
  63. package/web-app/dist/assets/{shield-alert-DLYLdVJ0.js → shield-alert-Du08zhdg.js} +1 -1
  64. package/web-app/dist/assets/{trash-2-Cc-VTvzt.js → trash-2-DEKSVae5.js} +1 -1
  65. package/web-app/dist/assets/{trending-down-CrDpO2a_.js → trending-down-DBiXUtxJ.js} +1 -1
  66. package/web-app/dist/assets/{trending-up-CNVsmM3G.js → trending-up-BgmK_tHq.js} +1 -1
  67. package/web-app/dist/assets/{upload-LuDuB7Wc.js → upload-IaViyeVD.js} +1 -1
  68. package/web-app/dist/assets/{usePolling-C8rvc-CG.js → usePolling-PiRLqNu6.js} +1 -1
  69. package/web-app/dist/assets/{user-BT79cI-o.js → user-BB5J8wAF.js} +1 -1
  70. package/web-app/dist/index.html +2 -3
  71. package/web-app/server.py +45 -7
  72. package/web-app/dist/assets/HomePage-CzeoS2V_.js +0 -28
  73. package/web-app/dist/assets/MagicPage-CBLqpa55.js +0 -31
  74. package/web-app/dist/assets/NotFoundPage-B62u4iCs.js +0 -1
  75. package/web-app/dist/assets/ProjectsPage-uHG7kxB-.js +0 -6
  76. package/web-app/dist/assets/SystemSettingsPage-C_Q_1WK4.js +0 -6
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.5.28
6
+ # Loki Mode v7.5.30
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.5.28 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.5.30 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.5.28
1
+ 7.5.30
package/autonomy/loki CHANGED
@@ -3783,7 +3783,7 @@ cmd_web_start() {
3783
3783
  existing_pid=$(cat "$PURPLE_LAB_PID_FILE" 2>/dev/null)
3784
3784
  if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
3785
3785
  echo -e "${GREEN}Purple Lab already running (PID: $existing_pid)${NC}"
3786
- local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
3786
+ local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/"
3787
3787
  echo -e "Open: ${CYAN}$url${NC}"
3788
3788
  if [ "$open_browser" = true ]; then
3789
3789
  if command -v open &> /dev/null; then
@@ -3842,7 +3842,7 @@ cmd_web_start() {
3842
3842
  local retries=0
3843
3843
  local server_ready=false
3844
3844
  while [ $retries -lt 15 ]; do
3845
- if curl -s "http://${PURPLE_LAB_DEFAULT_HOST}:${port}/api/session/status" > /dev/null 2>&1; then
3845
+ if curl -s "http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/api/session/status" > /dev/null 2>&1; then
3846
3846
  server_ready=true
3847
3847
  break
3848
3848
  fi
@@ -3857,7 +3857,7 @@ cmd_web_start() {
3857
3857
  exit 1
3858
3858
  fi
3859
3859
 
3860
- local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
3860
+ local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/"
3861
3861
 
3862
3862
  if [ "$server_ready" = true ]; then
3863
3863
  echo -e "${GREEN}Purple Lab running at: $url${NC} (PID: $pid)"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.5.28"
10
+ __version__ = "7.5.30"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -755,6 +755,25 @@ app.add_middleware(
755
755
  from .api_v2 import router as api_v2_router
756
756
  app.include_router(api_v2_router)
757
757
 
758
+ # Phase Merge-4: Mount Purple Lab FastAPI app under /lab/ so it appears as a
759
+ # sidebar entry in Dashboard. Same `app` is also wrapped by `standalone_app`
760
+ # in web-app/server.py for `loki web` (port 57375). One source of truth, no
761
+ # duplicated UIs. Import is best-effort: if web-app is missing (e.g. partial
762
+ # install) the dashboard still starts; /lab/* returns 404 with a clear hint.
763
+ _PURPLE_LAB_MOUNTED = False
764
+ try:
765
+ import sys as _sys
766
+ from pathlib import Path as _Path
767
+ _webapp_dir = _Path(__file__).resolve().parent.parent / "web-app"
768
+ if str(_webapp_dir) not in _sys.path:
769
+ _sys.path.insert(0, str(_webapp_dir))
770
+ import server as _purple_lab_server # type: ignore[import-not-found]
771
+ app.mount("/lab", _purple_lab_server.app)
772
+ _PURPLE_LAB_MOUNTED = True
773
+ logger.info("Purple Lab mounted at /lab/ (Phase Merge-4)")
774
+ except Exception as _e: # noqa: BLE001
775
+ logger.warning("Purple Lab NOT mounted (Phase Merge-4): %s -- /lab/ will 404", _e)
776
+
758
777
 
759
778
  # Health endpoint
760
779
  @app.get("/health")
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.5.28
5
+ **Version:** v7.5.30
6
6
 
7
7
  ---
8
8
 
@@ -0,0 +1,420 @@
1
+ # Purple Lab Into Dashboard: Deduplication Map (Phase Merge-2)
2
+
3
+ **Status:** Audit complete. Doc-only output for Phase Merge-2 of the v7.5.29+ true-integration arc.
4
+ **Date:** 2026-05-23
5
+ **Scope:** Duplicated business logic, state stores, file paths, session models, WebSocket buses, and auth infrastructure between `dashboard/` and `web-app/`.
6
+
7
+ This document is the source-of-truth deduplication roadmap. It enumerates every shared concept, identifies conflicts, and assigns a canonical version for each with the rationale. The merge into a single Loki Mode UI is Phase Merge-4. This audit completes the dependency analysis for Merge-5 (semantic dedup).
8
+
9
+ ---
10
+
11
+ ## 1. State Directory Paths
12
+
13
+ | Path | Dashboard Usage | Purple Lab Usage | Same Data? | Conflict if Both Write? | Canonical |
14
+ |---|---|---|---|---|---|
15
+ | `~/.loki/state/` | Dashboard only: orchestrator.json, session.json, provider, prd | Web-app sets `LOKI_DIR` per project: `<project>/.loki/state/` | NO: Dashboard monitors global state; Lab creates per-project state | YES: Dashboard writes global state; Lab writes local state inside project | Dashboard: Keep global state in `~/.loki/state/`. Lab: Write per-project to `<project>/.loki/state/` (already does via env var). Explicit namespace separation. |
16
+ | `~/.loki/dashboard/` | Token storage (tokens.json) at `dashboard/auth.py:32` | Purple Lab tokens stored at `web-app/server.py:7878` as `~/.loki/tokens/` | NO: Different files and locations | NO: Separate directories and files | Dashboard wins: keep `~/.loki/dashboard/tokens.json`. Lab redirects reads to Dashboard's auth endpoint (Merge-5). |
17
+ | `~/.loki/dashboard/audit/` | Dashboard audit logs at `dashboard/audit.py:7` | Web-app writes audit logs to DB (models.py, AuditLog table) | NO: Dashboard is file-based; Lab is DB-based | NO: Different storage backends | Dashboard wins for CLI audit trail (file-based); Lab uses DB for web-UI audit (will unify in Merge-5 via Dashboard audit API). |
18
+ | `~/.loki/purple-lab/child-pids.json` | N/A | Lab PID tracking at `web-app/server.py:223` | N/A | NO: Lab-specific, not written by Dashboard | Lab keeps this. Dashboard is unaware of Lab child processes. |
19
+ | `<project>/.loki/` | N/A | Web-app sets `LOKI_DIR=<project>/.loki` at `web-app/server.py:2663` | N/A | NO: Per-project state, isolated | Lab keeps this. Dashboard never touches per-project state. |
20
+ | `~/.loki/logs/` | Dashboard writes logs at `dashboard/control.py:32` | Web-app logs go to project-local dir (via LOKI_DIR) at `web-app/server.py` (implicit, via subprocess env) | NO: Different locations and scopes | NO: Separate directories | Dashboard: global logs. Lab: per-project logs. Both win (isolated scopes). |
21
+ | `~/.loki/migrations/` | Migration state at `dashboard/migration_engine.py:183` | N/A | N/A | N/A | Dashboard only. |
22
+
23
+ **Recommendation:** NO BREAKING CHANGES needed for Merge-4. The mount isolates via path prefix (`/lab/`). Lab's per-project state stays inside the project directory (via `LOKI_DIR` env var). Dashboard's global state stays at `~/.loki/`. The two trees do not collide because Dashboard reads global state and Lab reads per-project state.
24
+
25
+ ---
26
+
27
+ ## 2. Session Models (Pydantic)
28
+
29
+ ### Dashboard: `Session` (SQLAlchemy ORM)
30
+
31
+ **File:** `dashboard/models.py:179-203`
32
+
33
+ | Field | Type | Nullable | Notes |
34
+ |---|---|---|---|
35
+ | `id` | int (PK) | NO | Auto-increment |
36
+ | `project_id` | int (FK) | NO | Foreign key to Project |
37
+ | `status` | SessionStatus enum | NO | Values: ACTIVE, PAUSED, COMPLETED, FAILED |
38
+ | `provider` | str | NO | Default: "claude" |
39
+ | `model` | str | YES | Optional model override |
40
+ | `started_at` | datetime | NO | Server default: now() |
41
+ | `ended_at` | datetime | YES | NULL until session ends |
42
+ | `logs` | text | YES | Session logs (JSON or text) |
43
+ | Relationships | agents (1:N) | NO | Cascade delete |
44
+
45
+ ### Purple Lab: `Session` (SQLAlchemy ORM)
46
+
47
+ **File:** `web-app/models.py:63-78`
48
+
49
+ | Field | Type | Nullable | Notes |
50
+ |---|---|---|---|
51
+ | `id` | UUID | NO | Client-side generated |
52
+ | `user_id` | UUID (FK) | NO | Foreign key to User |
53
+ | `project_id` | UUID (FK) | YES | Foreign key to Project |
54
+ | `prd_content` | text | YES | Full PRD stored in DB |
55
+ | `provider` | str | NO | Default: "claude" |
56
+ | `mode` | str | NO | Default: "standard" |
57
+ | `status` | str | NO | Values: "created", "running", "paused", "completed", "failed" |
58
+ | `started_at` | datetime | NO | Default: utcnow() |
59
+ | `ended_at` | datetime | YES | NULL until session ends |
60
+ | `metadata_json` | JSON | NO | Flexible metadata dict |
61
+ | Relationships | user, project | NO | |
62
+
63
+ ### Compatibility Analysis
64
+
65
+ | Aspect | Dashboard | Lab | Compatible? | Action |
66
+ |---|---|---|---|---|
67
+ | **Primary Key** | int (auto) | UUID | NO | Lab uses user-facing UUIDs; Dashboard uses internal integers. Separate DBs (Merge-4 mounts, so no shared DB). No conflict. |
68
+ | **Status Field** | enum (SessionStatus) | string | PARTIAL | Dashboard: [ACTIVE, PAUSED, COMPLETED, FAILED]; Lab: "created", "running", "paused", "completed", "failed". Values drift. Map in API layer. |
69
+ | **Provider** | str | str | YES | Both default "claude". Compatible. |
70
+ | **Model** | optional str | NOT PRESENT | NO | Dashboard tracks model choice; Lab infers from provider. Add `model` field to Lab in Merge-5. |
71
+ | **Started/Ended** | datetime (server default) | datetime (utcnow) | YES | Functionally equivalent. |
72
+ | **Logs** | text field | NOT PRESENT | NO | Lab logs go to stdout/file, not DB. Store in metadata_json during Merge-5. |
73
+ | **User Tracking** | NOT PRESENT | user_id (FK) | NO | Dashboard is single-user (CLI); Lab is multi-user. Don't merge. Keep Lab's user_id. |
74
+ | **PRD Content** | NOT PRESENT | prd_content (text) | NO | Lab stores PRD in DB for re-use; Dashboard reads from file. Separate concerns. |
75
+
76
+ **Recommendation:**
77
+ - **DO NOT merge schemas.** Dashboard tracks **agent execution state** (provider, model, status, logs). Lab tracks **user sessions** (user, PRD, metadata).
78
+ - **Create a bridge table** in Merge-5: `DashboardSession` references `Lab.Session` + stores Dashboard-specific fields (model, logs_path, agent_list).
79
+ - **Status mapping layer:** Dashboard API wraps Lab session status strings into Dashboard enums for internal use.
80
+
81
+ ---
82
+
83
+ ## 3. WebSocket / Event Bus
84
+
85
+ ### Dashboard: `ConnectionManager` + Direct Broadcast
86
+
87
+ **File:** `dashboard/server.py:394-436`
88
+
89
+ ```python
90
+ class ConnectionManager:
91
+ active_connections: list[WebSocket] = []
92
+
93
+ async def connect(ws) -> bool
94
+ async def disconnect(ws)
95
+ async def broadcast(message: dict[str, Any])
96
+ ```
97
+
98
+ **Events Broadcast:**
99
+ - `state_update`: `.loki/` state changed (via file monitor at `:451-667`)
100
+ - `skill-session-update`: Fall-back when `dashboard-state.json` is missing (`:554`)
101
+ - PID-based liveness checks (`:515`)
102
+
103
+ **Route:** `@app.websocket("/ws")` at `:1824`
104
+
105
+ ### Purple Lab: File Watcher + Broadcast Callback
106
+
107
+ **File:** `web-app/server.py:420-530`
108
+
109
+ ```python
110
+ class FileEventDebouncer(FileSystemEventHandler):
111
+ def __init__(self, project_dir, broadcast_fn, loop)
112
+ def on_any_event(event) -> None
113
+ def _schedule_broadcast() -> None
114
+ ```
115
+
116
+ **Events Broadcast:**
117
+ - File system changes: `{event_type, path, timestamp}`
118
+ - Terminal output: `{type: "terminal", output, session_id}`
119
+ - Dev server output: `{type: "backend_output", data, session_id}`
120
+
121
+ **Routes:**
122
+ - `@app.websocket("/ws")` at `:6290`
123
+ - `@app.websocket("/ws/terminal/{session_id}")` at `:6370`
124
+
125
+ ### Compatibility Analysis
126
+
127
+ | Aspect | Dashboard | Lab | Compatible? |
128
+ |---|---|---|---|
129
+ | **Connection Manager** | Simple broadcast to all | File-system event handler + selective broadcast | NO: Different models (poll vs file-watch) |
130
+ | **Events** | State JSON changes, skill-session updates | File system + terminal output | NO: Different audiences |
131
+ | **Clients** | Dashboard UI (browser) | Lab UI (browser) + Terminal client | YES, but separate concerns |
132
+ | **Message Format** | `{message_type, data, ...}` | `{event_type, path, ...}` | PARTIAL: Different schemas |
133
+
134
+ **Recommendation:**
135
+ - **DO NOT unify in Merge-4.** Two distinct buses serve different purposes.
136
+ - **Merge-4:** Lab's `/ws` becomes `/lab/ws` (mount prefixing).
137
+ - **Merge-5:** Unify into a single **Unified Event Bus** (UEB):
138
+ - Dashboard clients listen to `/ws` for Dashboard state.
139
+ - Lab clients (mounted at `/lab`) listen to `/lab/ws` for Lab state.
140
+ - Eventually (Phase 7), cross-publish: Dashboard publishes `lab.session.started` events that Dashboard clients can consume for UI updates.
141
+
142
+ ---
143
+
144
+ ## 4. Auth / Session-Token Handling
145
+
146
+ ### Dashboard: Token-Based + OIDC (Optional)
147
+
148
+ **File:** `dashboard/auth.py:1-695`
149
+
150
+ - **Token storage:** `~/.loki/dashboard/tokens.json` (file-based, SHA256 hashed)
151
+ - **Token format:** `loki_<urlsafe(32 bytes)>`
152
+ - **Token validation:** `validate_token()` at `:346`
153
+ - **Scope hierarchy:** `*` > `control` > `write` > `read` (at `:53`)
154
+ - **OIDC support:** Optional via `LOKI_OIDC_ISSUER` + `LOKI_OIDC_CLIENT_ID` (`:36-41`)
155
+ - **Auth dependency:** `get_current_token()` at `:618`
156
+ - **CORS origins:** `LOKI_DASHBOARD_CORS` env var (server.py:732)
157
+ - **Root path cookies:** Not set (token-based only)
158
+
159
+ ### Purple Lab: JWT + OAuth (GitHub, Google)
160
+
161
+ **File:** `web-app/auth.py:1-210+` (truncated in read)
162
+
163
+ - **Token storage:** `~/.loki/tokens/` (file-based)
164
+ - **Token format:** JWT (via `python-jose`, `PURPLE_LAB_SECRET_KEY` at `:34`)
165
+ - **Token creation:** `create_access_token()` at `:58`
166
+ - **Token validation:** `verify_token()` at `:68`
167
+ - **OAuth callbacks:** `github_oauth_callback()` (`:158`), `google_oauth_callback()` (`:209`)
168
+ - **CORS origins:** `PURPLE_LAB_CORS_ORIGINS` env var (server.py:108)
169
+ - **Root path cookies:** Not set (JWT bearer token only)
170
+
171
+ ### Compatibility Analysis
172
+
173
+ | Aspect | Dashboard | Lab | Conflict if Both Write? |
174
+ |---|---|---|---|
175
+ | **Token Format** | `loki_<urlsafe>` (opaque) | JWT (introspectable) | NO: Different tokens, same purpose |
176
+ | **Token Storage** | `~/.loki/dashboard/tokens.json` | `~/.loki/tokens/` (implied) | NO: Different files |
177
+ | **Scope Model** | Role-based (admin, operator, viewer, auditor) | NOT PRESENT in web-app (all OIDC users get `["*"]`) | NO: Dashboard owns scopes. Lab uses DB users. |
178
+ | **OIDC Support** | Optional, via env vars | Implicit (OAuth), NOT OIDC | PARTIAL: Dashboard uses OIDC; Lab uses OAuth. |
179
+ | **Cookies** | NOT SET | NOT SET | NO: Both are stateless (token-based). |
180
+ | **Root-Path Auth** | Optional via `require_scope()` dependency | Optional via `get_current_user()` dependency | NO: Both use Bearer tokens. Same origin after mount means CORS becomes redundant. |
181
+
182
+ **Recommendation:**
183
+ - **DO NOT merge auth systems in Merge-4.** They serve different clients (CLI + Dashboard vs Web app).
184
+ - **Merge-4 action:** Dashboard's CORS middleware is redundant after mount (same origin). Remove `CORSMiddleware` from Lab when mounted.
185
+ - **Merge-5 action:** Unify token storage and validation:
186
+ - Canonical: Dashboard's `~/.loki/dashboard/tokens.json` for CLI API tokens.
187
+ - Lab users: Stored in DB (models.py:User table). Lab auth looks up user in DB, not file.
188
+ - No cross-auth: Dashboard API tokens ≠ Lab DB users. Separate realms.
189
+
190
+ ---
191
+
192
+ ## 5. CORS Middleware
193
+
194
+ | Server | CORS Enabled? | Origins | Env Var | Conflict? |
195
+ |---|---|---|---|---|
196
+ | **Dashboard** | YES | `http://localhost:57374,http://127.0.0.1:57374` (default) | `LOKI_DASHBOARD_CORS` | YES: Redundant after mount (same origin) |
197
+ | **Purple Lab** | YES | `http://localhost:57374,http://127.0.0.1:57374` (default) | `PURPLE_LAB_CORS_ORIGINS` | YES: Redundant after mount (same origin) |
198
+
199
+ **Code References:**
200
+ - Dashboard: `dashboard/server.py:728-746`
201
+ - Lab: `web-app/server.py:104-124`
202
+
203
+ **Recommendation:**
204
+ - **Merge-4:** Remove `CORSMiddleware` from Lab's FastAPI app when mounted. Dashboard's CORS middleware at root (`/`) handles the browser's same-origin policy.
205
+ - **Rationale:** After mount, Lab is at `/lab/*` and Dashboard is at `/` + `/api/*` + `/dashboard/*`. All served from the same origin (same host/port), so CORS is unnecessary.
206
+
207
+ ---
208
+
209
+ ## 6. Lifespan / Startup Events
210
+
211
+ ### Dashboard
212
+
213
+ **File:** `dashboard/server.py`
214
+
215
+ - **No `@app.on_event("startup")` found.** (grep returned empty)
216
+ - **Database init:** Via `init_db()` dependency injected at app scope (database.py, implicit)
217
+ - **Activity logger init:** `get_activity_logger()` called ad-hoc (activity_logger.py:31+)
218
+ - **Telemetry init:** `_telemetry` module imported, not explicitly initialized (telemetry.py:54)
219
+
220
+ ### Purple Lab
221
+
222
+ **File:** `web-app/server.py`
223
+
224
+ - **No `@app.on_event("startup")` found.** (grep returned empty)
225
+ - **Database init:** Via `init_db()` called in `database.py` (models.py:116-140)
226
+ - **Dev server managers init:** `dev_server_manager` and `dev_server_manager_v2` instantiated at module level (server.py:1571+)
227
+ - **Terminal manager init:** `terminals_manager` instantiated at module level (server.py:211)
228
+ - **PID tracker init:** `session = SessionState()` at module level (server.py:217)
229
+
230
+ ### Compatibility Analysis
231
+
232
+ | Component | Dashboard | Lab | Action |
233
+ |---|---|---|---|
234
+ | **Database init** | Via ORM session factory | Via async engine + async_session_factory | COMPATIBLE: Both async. No ordering required. |
235
+ | **Process managers** | N/A | Global instances (DevServerManager, TerminalManager) | NO CONFLICT: Lab-specific, not used by Dashboard. |
236
+ | **Activity logger** | Ad-hoc initialization | Not present | NO CONFLICT: Dashboard-only. |
237
+ | **Startup ordering** | Implicit (DB auto-init) | Implicit (global instances) | SAFE: No explicit hooks to compose. |
238
+
239
+ **Recommendation:**
240
+ - **Merge-4:** No changes needed. Both servers initialize implicitly via module-level globals and ORM lazy-loading.
241
+ - **Safe to mount:** Neither server has explicit lifespan hooks that could conflict.
242
+
243
+ ---
244
+
245
+ ## 7. Shared Python Utilities
246
+
247
+ ### Shared Modules (Used by Both)
248
+
249
+ | Module | Dashboard Import | Lab Import | Conflict? | Canonical |
250
+ |---|---|---|---|---|
251
+ | `memory/` | `dashboard/server.py:2376` reads `.loki/memory/` | `web-app/server.py:3121` reads `.loki/memory/` | NO: Both read-only, same path | Both. Memory system is read-only for both servers. |
252
+ | `events/` | `dashboard/control.py:422` (`emit_event()`) | NOT FOUND in web-app | NO: Dashboard-only event bus | Dashboard. Lab does not emit to dashboard event bus. |
253
+ | `providers/` | Used via CLI (loki start), not in server code | Used via subprocess (loki start) | NO: CLI-level, not server-level | N/A (both invoke CLI) |
254
+
255
+ ### Utility Functions
256
+
257
+ **Dashboard-only utilities:**
258
+ - `dashboard/control.py`: `atomic_write_json()` (`:41`), `get_status()` (`:60`), `emit_event()` (`:422`), `start_session()` (`:367`)
259
+ - `dashboard/registry.py`: Registry management for projects
260
+ - `dashboard/migration_engine.py`: Migration orchestration
261
+ - `dashboard/audit.py`: Audit logging
262
+
263
+ **Lab-only utilities:**
264
+ - `web-app/server.py`: `SessionState` (`:132`), `DevServerManager` (`:577`), `TerminalManager`, `ProjectFileManager`
265
+ - `web-app/models.py`: Async DB session factory
266
+
267
+ **No overlap detected.**
268
+
269
+ **Recommendation:**
270
+ - **DO NOT create shared utility modules in Merge-4.** Each server has distinct responsibilities.
271
+ - **Merge-5:** If cross-server calls are needed (e.g., Lab needs to call Dashboard's `get_status()`), use HTTP API, not shared Python modules.
272
+
273
+ ---
274
+
275
+ ## 8. Duplicated Business Functions
276
+
277
+ ### Critical Duplicates Found
278
+
279
+ #### `start_session()` -- DUPLICATED
280
+
281
+ | Aspect | Dashboard (control.py:367) | Purple Lab (server.py:2606) | Conflict? |
282
+ |---|---|---|---|
283
+ | **Purpose** | Start loki autonomy via run.sh (CLI-driven) | Start loki session via loki start/quick (Lab-driven) | YES: Different triggering paths for same action |
284
+ | **Input** | `StartRequest` (prd path, provider, options) | `StartRequest` (prd content as string, provider, mode) | PARTIAL: Different fields (prd_path vs prd_content) |
285
+ | **Process** | Spawns `run.sh` subprocess directly | Spawns `loki start` via CLI (which runs run.sh) | YES: Dashboard invokes run.sh; Lab invokes CLI which invokes run.sh |
286
+ | **State Tracking** | Saves provider to `STATE_DIR / "provider"` | Sets `LOKI_DIR` env var per project | PARTIAL: Different state models |
287
+ | **Event Emission** | Calls `emit_event("session_start", {...})` | No event emission (implicit via subprocess) | NO: Dashboard has event infrastructure; Lab doesn't |
288
+
289
+ **Impact:** Both try to start the same underlying `run.sh` process, but from different code paths:
290
+ - Dashboard: CLI → `loki dashboard` → Dashboard Server (FastAPI) → `start_session()` → `run.sh`
291
+ - Lab: Web UI → Lab Server (FastAPI) → `start_session()` → `loki start` → `run.sh`
292
+
293
+ **Problem:** After mount, both `/api/control/start` (Dashboard) and `/lab/api/session/start` (Lab) invoke the same `run.sh`. They compete for:
294
+ - Global state at `~/.loki/state/`
295
+ - Single global session (only one can run at a time per Dashboard design)
296
+
297
+ **Recommendation:**
298
+ - **Merge-4 DECISION POINT:** Choose ONE `start_session()` entry point.
299
+ - **Option A** (recommended): Lab wins. Dashboard's `/api/control/start` becomes a thin wrapper calling `/lab/api/session/start` via HTTP (Merge-5).
300
+ - **Option B:** Dashboard wins. Lab's route is removed or deprecated in favor of Dashboard's `start_session()` (breaks Lab's standalone mode).
301
+ - **Rationale for Option A:** Lab's `start_session()` is more feature-complete (project directory, PRD content, quick mode). Dashboard's version is simpler. Unify on Lab's logic; Dashboard can call it via HTTP API.
302
+
303
+ ---
304
+
305
+ ### Other Business Functions
306
+
307
+ #### `get_status()` / Status Endpoints
308
+
309
+ | Aspect | Dashboard | Lab | Duplicate? |
310
+ |---|---|---|---|
311
+ | **Endpoint** | `@app.get("/api/status")` (server.py:850) | No `/status` endpoint (only `/api/session/status`) | PARTIAL: Different scope |
312
+ | **Purpose** | System-wide status (PID, agent count, session count, DB connected) | Session status (running, paused, error) | NO: Different concepts |
313
+
314
+ **Recommendation:** NO DEDUP NEEDED. Dashboard reports system health. Lab reports session state. Different concerns.
315
+
316
+ ---
317
+
318
+ ## 9. Cookie / Session-Token Namespace
319
+
320
+ | Aspect | Dashboard | Lab | Conflict? |
321
+ |---|---|---|---|
322
+ | **Cookie-based sessions** | NOT USED (token-based only) | NOT USED (JWT bearer token only) | NO: Both stateless. No cookies set at root path. |
323
+ | **Cookie domain** | N/A | N/A | NO: No cookies to conflict. |
324
+ | **Token scope** | Bearer token in Authorization header | Bearer token in Authorization header | COMPATIBLE: Same transport, different validation. |
325
+
326
+ **Recommendation:**
327
+ - **Safe to mount.** Neither server sets cookies. Both use stateless Bearer tokens in `Authorization: Bearer <token>` header.
328
+ - **Merge-4:** No changes needed.
329
+
330
+ ---
331
+
332
+ ## 10. Summary Table: Duplicate Canonical Versions
333
+
334
+ | Duplicate | Location | Dashboard | Lab | Canonical | Reason |
335
+ |---|---|---|---|---|---|
336
+ | **start_session()** | control.py:367 vs server.py:2606 | Subprocess run.sh, state file tracking | Subprocess loki start, per-project state | **Lab wins** (more complete, feature-rich). Dashboard calls via HTTP (Merge-5). | Lab version handles projects, PRD content, quick mode. Dashboard version simpler. Unify on richer implementation. |
337
+ | **Session model** | models.py (both) | Execution state (provider, model, logs) | User session state (user_id, PRD, metadata) | **Keep separate** (different domains). | Dashboard tracks agent runs. Lab tracks user sessions. Bridge in Merge-5 via DashboardSession FK to Lab.Session. |
338
+ | **WebSocket bus** | server.py:394 vs server.py:420 | State polling + broadcast | File watch + event debouncer | **Keep separate** (different audiences). | Dashboard UI monitors .loki/ changes. Lab UI monitors project file changes. Unify message schema in Merge-5 (one UEB). |
339
+ | **Token auth** | auth.py (both) | Opaque loki_ tokens + OIDC | JWT tokens + OAuth | **Keep separate** (different clients). | Dashboard tokens for CLI API. Lab tokens for web users. Unify in Merge-5 at API layer (Dashboard token auth calls Lab JWT validation). |
340
+ | **CORS middleware** | server.py (both) | Enabled, port 57374 | Enabled, port 57374 | **Remove Lab's CORS** (redundant after mount). | Same origin after mount makes CORS unnecessary. Simplify. |
341
+ | **State paths** | Various | ~/.loki/state/ (global) | <project>/.loki/state/ (per-project) | **Keep both** (explicit namespace separation). | Dashboard state is global. Lab state is per-project. No collision. |
342
+
343
+ ---
344
+
345
+ ## Phase Merge-2 Deliverables
346
+
347
+ - [x] State directory path audit (section 1)
348
+ - [x] Session model compatibility analysis (section 2)
349
+ - [x] WebSocket/event bus architecture (section 3)
350
+ - [x] Auth/token namespace audit (section 4)
351
+ - [x] CORS middleware redundancy check (section 5)
352
+ - [x] Lifespan/startup hooks composition (section 6)
353
+ - [x] Shared utilities discovery (section 7)
354
+ - [x] Duplicate business functions (section 8)
355
+ - [x] Cookie/session-token conflicts (section 9)
356
+ - [x] Deduplication recommendations with canonical versions (section 10)
357
+
358
+ ---
359
+
360
+ ## Honest Acknowledgements
361
+
362
+ ### What Was NOT Audited (and Why)
363
+
364
+ 1. **Frontend code duplication** (TypeScript/React):
365
+ - Dashboard UI (`dashboard-ui/src/`) and Lab UI (`web-app/dist/` or source) likely have overlapping components (session cards, status displays, log viewers).
366
+ - Scope: This audit covers Python backend only. Frontend audit deferred to UI design review in Merge-3 (Vite rebuild).
367
+
368
+ 2. **Database schema migrations and compatibility:**
369
+ - Dashboard uses SQLAlchemy ORM with explicit schema (models.py).
370
+ - Lab uses Alembic migrations (migrations/versions/).
371
+ - No shared database in Merge-4 (separate instances). DB unification is a Merge-5+ decision.
372
+ - Scope: This audit assumes separate DBs per server (no merge needed in Merge-4).
373
+
374
+ 3. **Subprocess environment variables and cross-server communication:**
375
+ - Lab sets `LOKI_DIR` when spawning `loki start`. Dashboard sets environment for `run.sh`.
376
+ - No audit of whether subprocess reads Dashboard state or vice versa.
377
+ - Scope: Assumed to be isolated per server (no cross-reading of env vars).
378
+
379
+ 4. **Test suite duplication:**
380
+ - `dashboard/tests/` and `web-app/tests/` may have duplicate test cases.
381
+ - Scope: Not audited. Test refactoring in Phase Merge-8 (regression).
382
+
383
+ 5. **API endpoint overlap beyond routes:**
384
+ - Phase Merge-1 confirmed NO /api/* route collisions (paths are distinct).
385
+ - This audit did not deep-dive into semantic overlap (e.g., both have "get session info" but different response schemas).
386
+ - Scope: Merge-1 route audit sufficient. Semantic overlap is Merge-5 work.
387
+
388
+ 6. **Configuration file conflicts:**
389
+ - `.env`, `.loki/config.json`, or other config files may conflict.
390
+ - Scope: Not audited. Assumed env vars and filesystem state isolation is sufficient for Merge-4.
391
+
392
+ 7. **Dependency version skew:**
393
+ - Both servers require `fastapi`, `pydantic`, `sqlalchemy`, etc. Version mismatches not audited.
394
+ - Scope: Assumed CI/poetry lock files handle version alignment.
395
+
396
+ 8. **Logging output verbosity and timestamp format:**
397
+ - Dashboard logs go to `~/.loki/logs/`. Lab logs go to project-local or stdout.
398
+ - Log format (JSON, plain text, timestamps) not audited.
399
+ - Scope: Not critical for Merge-4 mount. Unify in Merge-5 via structured logging.
400
+
401
+ ---
402
+
403
+ ## Next Phases
404
+
405
+ **Merge-3:** Vite rebuild with `base: '/lab/'` (frontend routing setup).
406
+
407
+ **Merge-4:** FastAPI mount Lab into Dashboard (no code changes needed based on this audit).
408
+
409
+ **Merge-5 Deep Dedup Tasks (after Merge-4 mount is live):**
410
+ 1. Unify `start_session()` entry points (recommendation: Lab wins).
411
+ 2. Bridge Dashboard and Lab session models (DashboardSession FK).
412
+ 3. Unify event bus into single Loki Mode UEB (with routing by prefix).
413
+ 4. Consolidate CORS/auth at Dashboard root level.
414
+ 5. Map session status strings (Dashboard enum ↔ Lab string).
415
+
416
+ ---
417
+
418
+ **Audit completed by:** SDET (Senior Development Engineer in Test)
419
+ **Confidence:** HIGH (source code inspection + pattern matching)
420
+ **Blockers for Merge-4:** NONE. Mount is safe to proceed.
@@ -0,0 +1,139 @@
1
+ # Purple Lab Into Dashboard: Route Map (Phase Merge-1)
2
+
3
+ **Status:** Audit complete. Doc-only output for Phase Merge-1 of the v7.5.29+ true-integration arc.
4
+ **Date:** 2026-05-23
5
+ **Source:** `dashboard/server.py` (133 routes) and `web-app/server.py` (112 routes), audited from HEAD.
6
+
7
+ This document is the source-of-truth route-collision analysis for the Purple Lab merge into Dashboard. It enumerates every route in both servers, identifies collisions, and defines the namespace strategy for Phases Merge-2 through Merge-7.
8
+
9
+ ## Top-Level Summary
10
+
11
+ | Metric | Value |
12
+ |---|---|
13
+ | Dashboard routes -- main `app` decorators | 133 |
14
+ | Dashboard routes -- `api_v2_router` (mounted at `/api/v2/*` via `include_router`) | 24 |
15
+ | Dashboard routes -- total | **157** |
16
+ | Dashboard `/api/*` routes | 135 |
17
+ | Purple Lab routes (total) | 112 |
18
+ | Purple Lab `/api/*` routes | 100 |
19
+ | Purple Lab routers (`APIRouter` / `include_router`) | 0 |
20
+ | Exact collisions (method + path) | **3** |
21
+ | Path-only collisions | **3** |
22
+ | `/api/*` collisions (any method) | **0** |
23
+ | `/api/v2/*` collisions vs Purple Lab | **0** (Purple Lab has no `/api/v2/*` routes) |
24
+ | Surviving uncollided Purple Lab routes | **109** |
25
+
26
+ **Result:** The merge is structurally clean. Only 3 infrastructure paths collide; all 100 of Purple Lab's `/api/*` routes have distinct paths from Dashboard's 135 `/api/*` routes. The `api_v2_router` (24 routes at `/api/v2/tenants`, `/api/v2/runs`, `/api/v2/api-keys`, `/api/v2/policies`, `/api/v2/audit`) is enterprise multi-tenant surface that Purple Lab does not currently expose.
27
+
28
+ ## The 3 Collisions
29
+
30
+ | Method | Path | Dashboard purpose | Purple Lab purpose | Resolution |
31
+ |---|---|---|---|---|
32
+ | `GET` | `/health` | Dashboard health check | Purple Lab health check | Single `/health` on Dashboard wins. Lab's becomes `/lab/health`. |
33
+ | `GET` | `/{full_path:path}` | Dashboard SPA catch-all (serves `dashboard/static/index.html`) | Purple Lab SPA catch-all (serves `web-app/dist/index.html`) | Dashboard catch-all wins at root. Lab's becomes `/lab/{full_path:path}` -- triggered only inside the mount. |
34
+ | `WS` | `/ws` | Dashboard WebSocket bus | Purple Lab WebSocket bus | Dashboard `/ws` wins. Lab's becomes `/lab/ws`. Long term: unify into a single bus (Phase Merge-5). |
35
+
36
+ All three collisions resolve automatically when Lab's FastAPI app is mounted at `/lab` via Starlette `app.mount("/lab", purple_lab_app)`. No code dedup needed in Merge-1 -- the mount provides path isolation.
37
+
38
+ ## Namespace Strategy
39
+
40
+ Single rule: **Purple Lab's entire FastAPI app mounts under `/lab/`**. No route renames inside the Lab app itself -- the mount handles prefixing.
41
+
42
+ - Lab route `/api/sessions/{id}/chat` becomes externally visible as `/lab/api/sessions/{id}/chat`.
43
+ - Lab static asset `/assets/index-BN52-GQT.js` becomes `/lab/assets/index-BN52-GQT.js`.
44
+ - Lab WebSocket `/ws` becomes `/lab/ws`.
45
+
46
+ The Vite build (Phase Merge-3) is rebuilt with `base: '/lab/'` so the bundled JS naturally calls `/lab/api/*` and the HTML loads `/lab/assets/*`. No runtime base-href shimming needed.
47
+
48
+ ## Web-App Route Inventory (by category)
49
+
50
+ | Category | Count | Sample paths |
51
+ |---|---|---|
52
+ | `/api/sessions/*` | 61 | `/api/sessions/{session_id}/chat/{task_id}/stream`, `/api/sessions/{session_id}/checkpoints` |
53
+ | `/api/session/*` | 18 | `/api/session/start`, `/api/session/quick-start`, `/api/session/status` |
54
+ | `/api/deploy/*` | 6 | `/api/deploy/{provider}` |
55
+ | `/api/magic/*` | 5 | `/api/magic/components`, `/api/magic/generate` |
56
+ | `/api/auth/*` | 5 | (auth endpoints) |
57
+ | `/api/teams/*` | 4 | (team management) |
58
+ | `/api/secrets/*` | 3 | (secrets management) |
59
+ | `/api/templates/*` | 2 | `/api/templates`, `/api/templates/{filename}` |
60
+ | `/api/provider/*` | 2 | `/api/provider/current`, `/api/provider/set` |
61
+ | `/api/audit-log` | 1 | (audit log endpoint) |
62
+ | `/proxy/{session_id}` | 1 | (legacy proxy) |
63
+ | `/ws/*` | 2 | `/ws`, `/ws/terminal` |
64
+ | `/health` | 1 | (health check) |
65
+ | `/{full_path:path}` | 1 | (SPA catch-all) |
66
+ | **Total** | **112** | |
67
+
68
+ ## Dashboard Route Inventory (by category, top 20)
69
+
70
+ | Category | Count |
71
+ |---|---|
72
+ | `/api/memory/*` | 14 |
73
+ | `/api/registry/*` | 10 |
74
+ | `/api/learning/*` | 9 |
75
+ | `/api/migration/*` | 8 |
76
+ | `/api/council/*` | 8 |
77
+ | `/api/tasks/*` | 6 |
78
+ | `/api/enterprise/*` | 6 |
79
+ | `/api/projects/*` | 5 |
80
+ | `/api/control/*` | 5 |
81
+ | `/api/checklist/*` | 5 |
82
+ | `/api/notifications/*` | 4 |
83
+ | `/api/agents/*` | 4 |
84
+ | `/api/managed/*` | 3 |
85
+ | `/api/github/*` | 3 |
86
+ | `/api/focus/*` | 3 |
87
+ | `/api/checkpoints/*` | 3 |
88
+ | (etc., 89 more under /api/) | -- |
89
+ | **Total** | **133** |
90
+
91
+ ## Semantic Overlap (Candidates for Dedup in Phase Merge-5)
92
+
93
+ After path-suffix analysis, only one path-suffix is genuinely shared between the two servers:
94
+
95
+ - `github/status` -- appears as `/api/github/status` (Dashboard) and inside Lab's session endpoints. Not a collision (different paths), but functional overlap worth investigation in Phase Merge-5.
96
+
97
+ The bigger semantic overlap is at the conceptual level:
98
+
99
+ | Concept | Dashboard endpoint | Purple Lab endpoint | Merge-5 decision needed |
100
+ |---|---|---|---|
101
+ | Session lifecycle (start/stop/status) | `/api/control/*` (5 routes) | `/api/session/*` (18 routes) + `/api/sessions/{id}/*` (61 routes) | Likely: Lab keeps richer session CRUD; Dashboard's `/api/control/*` becomes thin wrappers calling Lab's. Or: unify on Lab's surface and retire Dashboard's. Decide in Merge-5. |
102
+ | Memory access | `/api/memory/*` (14 routes) | `/api/session/{id}/memory` (1 route) | Dashboard owns the rich surface; Lab's becomes a thin wrapper. |
103
+ | Checkpoints | `/api/checkpoints/*` (3 routes) | `/api/sessions/{id}/checkpoints` (2 routes) | Dashboard owns the global view; Lab's per-session view consumes Dashboard's. |
104
+ | Council / quality | `/api/council/*` (8) + `/api/quality-score/*` (2) | -- | Dashboard-only. Lab needs to surface this via UI, not duplicate endpoints. |
105
+ | Provider routing | `/api/provider/*` (?) | `/api/provider/current`, `/api/provider/set` | Likely same routes already; verify in Merge-2. |
106
+
107
+ None of these block the mount in Merge-4. They become refactor targets in Merge-5.
108
+
109
+ ## Phase Dependencies (Confirmed by This Audit)
110
+
111
+ - **Merge-2** (state/business-logic dedup audit): Doable in parallel with Merge-3. Operates on `.loki/state/` and Python modules, not routes.
112
+ - **Merge-3** (Vite rebuild with `base: '/lab/'`): Single-file config change in `web-app/vite.config.ts` + `npm run build`. No route changes needed because the mount handles prefixing.
113
+ - **Merge-4** (FastAPI mount): The 3 infrastructure collisions resolve naturally via mount path isolation. No route renames inside `web-app/server.py`.
114
+ - **Merge-5** (deep dedup): Only required for the 4 conceptual overlaps above. Each becomes a separate sub-task.
115
+ - **Merge-6** (sidebar entry): Pure dashboard-UI work, no backend route changes.
116
+ - **Merge-7** (deprecate `loki web` standalone): Keep `loki web` working for 2 minor versions per user-safety Rule 0. Standalone uvicorn entrypoint is preserved; only `loki dashboard` gains the mount.
117
+
118
+ ## NOT Decided In This Phase
119
+
120
+ 1. **Cookie / session-token namespace.** If Lab and Dashboard both set cookies at root path, mount may cause conflicts. Audit needed in Merge-2.
121
+ 2. **CORS middleware.** Lab has CORS middleware at line 119 of `web-app/server.py`. After mount, this becomes redundant (same origin). Remove in Merge-4.
122
+ 3. **Lifespan / startup events.** Lab's startup hooks (if any) must compose with Dashboard's. Audit in Merge-4.
123
+ 4. **Auth headers.** If Lab requires its own auth token and Dashboard requires another, the mount may double-auth or break. Audit in Merge-2.
124
+
125
+ These are not blockers for Merge-1 sign-off; they are explicit followups.
126
+
127
+ ## Cleanup
128
+
129
+ Audit artifacts in `/tmp/loki-merge-audit/` may be removed:
130
+ ```bash
131
+ rm -rf /tmp/loki-merge-audit
132
+ ```
133
+
134
+ ## Honest Acknowledgements
135
+
136
+ - The "0 /api/* collisions" finding holds for the route-table inspection only. Two routes with distinct paths can still share a backing function or `.loki/` file, and that semantic overlap is what Phase Merge-2 must catalog.
137
+ - Initial audit used `grep -nE '^@app\.(get|post|put|delete|patch|websocket)\(' ...` which missed `include_router` mounts. Followup audit found Dashboard's `api_v2_router` at `dashboard/api_v2.py` (24 routes, prefix `/api/v2`) and confirmed Purple Lab has zero `APIRouter` / `include_router` registrations. The summary table reflects the corrected total of 157 Dashboard routes.
138
+ - `app.add_api_route(...)` is also a dynamic registration pattern. `grep -n "add_api_route" web-app/server.py dashboard/server.py` returns nothing -- both servers use decorator-style registration exclusively.
139
+ - No fixture-based runtime collision test was performed. Phase Merge-4 must add a route-table dump test that runs the mounted app and asserts the surviving route set against this doc's expected output.