loki-mode 7.5.27 → 7.5.29

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 (77) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/lib/project-graph.sh +24 -14
  4. package/autonomy/loki +50 -18
  5. package/bin/loki +2 -1
  6. package/dashboard/__init__.py +1 -1
  7. package/docs/INSTALLATION.md +1 -1
  8. package/docs/MERGE-DEDUP-MAP.md +420 -0
  9. package/docs/MERGE-ROUTE-MAP.md +139 -0
  10. package/docs/MERGE3-PLAN.md +119 -0
  11. package/loki-ts/dist/loki.js +196 -171
  12. package/mcp/__init__.py +1 -1
  13. package/package.json +2 -2
  14. package/web-app/dist/assets/{AdminPage-DwVUK4v9.js → AdminPage-CKUOsWZW.js} +3 -3
  15. package/web-app/dist/assets/{Avatar-B7gqhcg3.js → Avatar-CL9Id9Hi.js} +1 -1
  16. package/web-app/dist/assets/{Badge-DA3xNJAS.js → Badge-B12zwlD7.js} +1 -1
  17. package/web-app/dist/assets/{Button-BPXURLaK.js → Button-CFLVoduT.js} +1 -1
  18. package/web-app/dist/assets/{ComparePage-B0JQMhKG.js → ComparePage-Dg0UdZAk.js} +1 -1
  19. package/web-app/dist/assets/{GitHubIssuesPanel-D38-fy29.js → GitHubIssuesPanel-CSitxtAX.js} +2 -2
  20. package/web-app/dist/assets/{GitHubPRsPanel-DLPcW3N0.js → GitHubPRsPanel-BIT06FRo.js} +1 -1
  21. package/web-app/dist/assets/HomePage-pU_0fGny.js +28 -0
  22. package/web-app/dist/assets/{LoginPage-DqCzxsfx.js → LoginPage-DTZtt2Yb.js} +1 -1
  23. package/web-app/dist/assets/MagicPage-10zfra8o.js +31 -0
  24. package/web-app/dist/assets/{MetricsPage-CPYQR0zr.js → MetricsPage-C-wiKUkv.js} +1 -1
  25. package/web-app/dist/assets/NotFoundPage-BDkcmhYe.js +1 -0
  26. package/web-app/dist/assets/{ProjectPage-DNujSl6j.js → ProjectPage-CiCavQ8n.js} +71 -71
  27. package/web-app/dist/assets/ProjectsPage-BLCXQwwC.js +6 -0
  28. package/web-app/dist/assets/{SettingsPage-BaQJbOgL.js → SettingsPage-PkxtaMyg.js} +3 -3
  29. package/web-app/dist/assets/{ShowcasePage-DQR_e-kg.js → ShowcasePage-iECp8Tha.js} +1 -1
  30. package/web-app/dist/assets/SystemSettingsPage-DS6Anno1.js +6 -0
  31. package/web-app/dist/assets/{TeamsPage-DOFErDqX.js → TeamsPage-ls6h6bNL.js} +1 -1
  32. package/web-app/dist/assets/{TemplatesPage-Ty72hILN.js → TemplatesPage-Bk0QzlPt.js} +3 -3
  33. package/web-app/dist/assets/{TerminalOutput-DqOVnR1p.js → TerminalOutput-4-1hWCtZ.js} +1 -1
  34. package/web-app/dist/assets/{activity-BgBZ4s4c.js → activity-DH3ih2nS.js} +1 -1
  35. package/web-app/dist/assets/{bell-C-UezVWi.js → bell-Gn17S6uv.js} +1 -1
  36. package/web-app/dist/assets/{bot-D70fEnm5.js → bot-Cbycc3VE.js} +1 -1
  37. package/web-app/dist/assets/{check-CBohulxQ.js → check-nIAqa-kf.js} +1 -1
  38. package/web-app/dist/assets/{chevron-left-C-emzUhB.js → chevron-left-D2jcWDll.js} +1 -1
  39. package/web-app/dist/assets/{circle-alert-8SRY0_GX.js → circle-alert-CpL4Bhvt.js} +1 -1
  40. package/web-app/dist/assets/{clock-mfq4XnPQ.js → clock-IW4Wq86N.js} +1 -1
  41. package/web-app/dist/assets/{cloud-DpRM7T8t.js → cloud-Cn8nNuH2.js} +1 -1
  42. package/web-app/dist/assets/{code-xml-1N2Ui-4c.js → code-xml-BiJBteXf.js} +1 -1
  43. package/web-app/dist/assets/{copy-LXquTgzI.js → copy-CnqkyNsi.js} +1 -1
  44. package/web-app/dist/assets/{database-S1dyXnuT.js → database-CKSReqa5.js} +1 -1
  45. package/web-app/dist/assets/{dollar-sign-CRqk0dW5.js → dollar-sign-CDzDY64R.js} +1 -1
  46. package/web-app/dist/assets/{file-code-corner-B99CwY_6.js → file-code-corner-Box4IwG1.js} +1 -1
  47. package/web-app/dist/assets/{file-plus-DZ5qnz5b.js → file-plus-DpGqlXF8.js} +1 -1
  48. package/web-app/dist/assets/{folder-open-DBCm7yuF.js → folder-open-B57dAoBv.js} +1 -1
  49. package/web-app/dist/assets/{git-commit-horizontal-DM1ERuNd.js → git-commit-horizontal-BVbucmO5.js} +1 -1
  50. package/web-app/dist/assets/{globe-B7xEJSL_.js → globe-BkOnKl4x.js} +1 -1
  51. package/web-app/dist/assets/{hammer-Cgi3LTuS.js → hammer-DRbIQ4QU.js} +1 -1
  52. package/web-app/dist/assets/{index-BN52-GQT.js → index-CM_b_EhP.js} +77 -77
  53. package/web-app/dist/assets/{layers-Bi8RPIBC.js → layers-B78BiFiU.js} +1 -1
  54. package/web-app/dist/assets/{lightbulb-Doc_n8JX.js → lightbulb-B-Itbm9g.js} +1 -1
  55. package/web-app/dist/assets/{loader-circle-BB932A7A.js → loader-circle-Oq6NQhW2.js} +1 -1
  56. package/web-app/dist/assets/{lock-Bt6gpMrs.js → lock-DbJ9zxbw.js} +1 -1
  57. package/web-app/dist/assets/{mail-BuzAu1IP.js → mail-CzMRod6m.js} +1 -1
  58. package/web-app/dist/assets/{package-BE5FHxQ8.js → package-WZ5osvej.js} +1 -1
  59. package/web-app/dist/assets/{plus-CNqABexN.js → plus-j08lFR-K.js} +1 -1
  60. package/web-app/dist/assets/{refresh-cw-34B13ztx.js → refresh-cw-CIr7E-g2.js} +1 -1
  61. package/web-app/dist/assets/{rotate-ccw-CrD2QB29.js → rotate-ccw-gwoXxDeE.js} +1 -1
  62. package/web-app/dist/assets/{save-DsJcqdnI.js → save-B8fV_ZpE.js} +1 -1
  63. package/web-app/dist/assets/{server-BcgRMArA.js → server-D5dO1paz.js} +1 -1
  64. package/web-app/dist/assets/{shield-alert-DLYLdVJ0.js → shield-alert-Du08zhdg.js} +1 -1
  65. package/web-app/dist/assets/{trash-2-Cc-VTvzt.js → trash-2-DEKSVae5.js} +1 -1
  66. package/web-app/dist/assets/{trending-down-CrDpO2a_.js → trending-down-DBiXUtxJ.js} +1 -1
  67. package/web-app/dist/assets/{trending-up-CNVsmM3G.js → trending-up-BgmK_tHq.js} +1 -1
  68. package/web-app/dist/assets/{upload-LuDuB7Wc.js → upload-IaViyeVD.js} +1 -1
  69. package/web-app/dist/assets/{usePolling-C8rvc-CG.js → usePolling-PiRLqNu6.js} +1 -1
  70. package/web-app/dist/assets/{user-BT79cI-o.js → user-BB5J8wAF.js} +1 -1
  71. package/web-app/dist/index.html +2 -3
  72. package/web-app/server.py +45 -7
  73. package/web-app/dist/assets/HomePage-CzeoS2V_.js +0 -28
  74. package/web-app/dist/assets/MagicPage-CBLqpa55.js +0 -31
  75. package/web-app/dist/assets/NotFoundPage-B62u4iCs.js +0 -1
  76. package/web-app/dist/assets/ProjectsPage-uHG7kxB-.js +0 -6
  77. package/web-app/dist/assets/SystemSettingsPage-C_Q_1WK4.js +0 -6
@@ -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.
@@ -0,0 +1,119 @@
1
+ # Phase Merge-3 Implementation Plan: Vite Rebuild with `/lab/` Base
2
+
3
+ **Status:** Architect-approved 2026-05-23. Dev fleet implements per this plan.
4
+ **Owner:** Phase Merge-3 of the v7.5.29+ Purple-Lab-into-Dashboard true-integration arc.
5
+ **Predecessors:** `docs/MERGE-ROUTE-MAP.md` (Merge-1), `docs/MERGE-DEDUP-MAP.md` (Merge-2).
6
+
7
+ ## Binding Decisions
8
+
9
+ 1. **Vite `base: '/lab/'`.** Single config change applies in dev + build.
10
+ 2. **API base via `import.meta.env.BASE_URL`.** No `/lab/` hardcoded in TypeScript -- the env var resolves to `/lab/` at build time and `/` in tests.
11
+ 3. **`loki web` standalone server ALSO mounts its app under `/lab/`.** Same bundle, same routing model as the merged Dashboard. Root `/` redirects to `/lab/`. Rule 0 preserved by URL shift only.
12
+ 4. **No duplicated dist artifacts.** Single `web-app/dist/` shipped to npm + Docker. No `base: '/'` second build.
13
+
14
+ ## File Diffs (concrete)
15
+
16
+ ### 1. `web-app/vite.config.ts`
17
+ ```diff
18
+ export default defineConfig({
19
+ plugins: [react()],
20
+ + base: '/lab/',
21
+ resolve: { alias: { '@': path.resolve(__dirname, './src') } },
22
+ server: {
23
+ port: 5173,
24
+ proxy: {
25
+ - '/api': { target: 'http://localhost:57375', changeOrigin: true },
26
+ - '/proxy': { target: 'http://localhost:57375', changeOrigin: true, ws: true },
27
+ - '/ws': { target: 'ws://localhost:57375', ws: true },
28
+ + '/lab/api': { target: 'http://localhost:57375', changeOrigin: true },
29
+ + '/lab/proxy': { target: 'http://localhost:57375', changeOrigin: true, ws: true },
30
+ + '/lab/ws': { target: 'ws://localhost:57375', ws: true },
31
+ },
32
+ },
33
+ })
34
+ ```
35
+
36
+ ### 2. `web-app/src/api/client.ts` (line 3 + line 7 area)
37
+ ```diff
38
+ -const API_BASE = (import.meta as any).env?.VITE_API_BASE
39
+ - || `${window.location.origin}/api`;
40
+ +const BASE = (import.meta as any).env?.BASE_URL || '/';
41
+ +const API_BASE = (import.meta as any).env?.VITE_API_BASE
42
+ + || `${window.location.origin}${BASE}api`;
43
+ +const WS_BASE = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${BASE}ws`;
44
+ ```
45
+ Then export `WS_BASE` and route any other websocket constructor through it.
46
+
47
+ ### 3. `web-app/src/pages/MagicPage.tsx` (4 hardcoded fetches at lines 43, 65, 86, 106)
48
+ Convert each `fetch('/api/magic/...')` to use a shared base. Cleanest fix: import the client API base:
49
+ ```diff
50
+ - const res = await fetch('/api/magic/components');
51
+ + const res = await fetch(`${import.meta.env.BASE_URL}api/magic/components`);
52
+ ```
53
+ Apply to all 4 sites. Or extract a `magicApi` helper in `web-app/src/api/client.ts`.
54
+
55
+ ### 4. `web-app/src/components/TerminalEmulator.tsx` (line 49)
56
+ ```diff
57
+ - const wsUrl = `${protocol}//${window.location.host}/ws/terminal/${sessionId}`;
58
+ + const wsUrl = `${protocol}//${window.location.host}${import.meta.env.BASE_URL}ws/terminal/${sessionId}`;
59
+ ```
60
+
61
+ ### 5. `web-app/server.py` -- standalone mount (added in Merge-4; staged here)
62
+
63
+ The architect's option 2: standalone server self-mounts at `/lab/`. The actual wiring lives in Merge-4, but Merge-3 needs to verify the URL flip works for `loki web`. For Merge-3 alone, we accept the standalone `loki web` URL changes to `http://127.0.0.1:57375/lab/`. Browser-open auto-redirect is added in Merge-4.
64
+
65
+ ### 6. `autonomy/loki` `cmd_web_start` browser-open URL (line ~3694)
66
+ Change the `open` URL from `http://127.0.0.1:${PURPLE_LAB_DEFAULT_PORT}/` to `http://127.0.0.1:${PURPLE_LAB_DEFAULT_PORT}/lab/`.
67
+
68
+ ## Release Pipeline Wiring
69
+
70
+ ### 7. Root `package.json` `prepublishOnly` (line ~106)
71
+ Append `&& cd ../web-app && npm ci && npm run build && test -f dist/index.html` after the existing dashboard-ui build.
72
+
73
+ ### 8. `Dockerfile`
74
+ Add `COPY` for web-app build artifacts + server files. Mirror the `dashboard/` COPY pattern. (Merge-4 owns the actual import; Merge-3 ensures files are in the image.)
75
+
76
+ ### 9. `scripts/local-ci.sh`
77
+ Add a check between existing steps:
78
+ ```bash
79
+ run_check "web-app build produces /lab/-prefixed assets" \
80
+ '(cd web-app && npm ci --silent && npm run build) && grep -q "/lab/assets/" web-app/dist/index.html'
81
+ ```
82
+
83
+ ## Tests (Merge-3 acceptance)
84
+
85
+ | ID | Test | Type |
86
+ |---|---|---|
87
+ | T1 | `grep -q '/lab/assets/' web-app/dist/index.html` after `npm run build` | bash, local-ci |
88
+ | T2 | `grep -q '/lab/api' web-app/dist/assets/index-*.js` (runtime base baked in) | bash, local-ci |
89
+ | T3 | `loki web --no-open; curl -sL http://127.0.0.1:57375/lab/ \| grep -q '<div id="root">'` | bash |
90
+ | T4 | `curl -s http://127.0.0.1:57375/lab/assets/index-*.js` returns JS not 404 | bash |
91
+ | T5 | Playwright: visit `http://127.0.0.1:57375/lab/`, no console 404s, screenshot HomePage baseline | Playwright |
92
+ | T6 | (Post-Merge-4) visit `http://127.0.0.1:57374/lab/`, screenshot diff vs T5 pixel-identical | Playwright |
93
+ | T7 | `curl -s http://127.0.0.1:57374/` returns Dashboard root unchanged | bash |
94
+ | T8 | `npm pack --dry-run \| grep web-app/dist/index.html` succeeds | local-ci |
95
+
96
+ ## Risks (with mitigations)
97
+
98
+ 1. **Hardcoded `/api/` strings drift back over time.** Mitigation: add local-ci grep guard:
99
+ `! grep -rn "['\"]/api\|['\"]/ws" web-app/src/ | grep -v "BASE_URL\|external"`
100
+
101
+ 2. **`/vite.svg` favicon 404 under `/lab/`.** Mitigation: verify `web-app/public/vite.svg` exists; if not, remove the `<link rel="icon" href="/vite.svg">` line from `web-app/index.html`.
102
+
103
+ 3. **Dev workflow break (`http://localhost:5173/` returns 404).** Mitigation: document `http://localhost:5173/lab/` in `web-app/README.md`; optionally add Vite middleware to redirect `/` -> `/lab/` in dev.
104
+
105
+ 4. **Cookies set at root path.** Mitigation: Merge-2 audit confirmed no cookies (Bearer tokens only). Re-verify with curl post-build.
106
+
107
+ 5. **Stale dist in git.** Mitigation: local-ci T1/T2 catch it. Stronger: pre-push hook that runs `npm run build` if src is newer than dist (deferred to follow-up).
108
+
109
+ ## NOT Done In This Phase
110
+
111
+ - Auto-spawn web-app subprocess from `loki dashboard` (Merge-4).
112
+ - Sidebar entry in `dashboard/static/index.html` (Merge-6).
113
+ - Deep state dedup (Merge-5).
114
+ - Deprecation of `loki web` (Merge-7).
115
+ - `vite-plugin-html` dev `/` -> `/lab/` redirect (nice-to-have, not blocking).
116
+
117
+ ## Acceptance Gate
118
+
119
+ Merge-3 ships when: T1-T4 + T7 + T8 green, no `git status` noise outside the planned files, `loki web --no-open` serves the Lab UI at `/lab/` end-to-end with browser-side console clean (verified by Playwright T5).